Skip to main content

Seeed Studio XIAO nRF54L15 Sense 引脚复用

为了便于使用,以下所有引脚复用示例都基于 PlatformIO。请点击此链接查看 XIAO nRF54L5 的配置和使用指南

tip

基于 VS Code,如果您想在 nRF Connect SDK 上使用以下案例,请参考提供的连接,添加 app.overlay 文件并修改 prj.conf 中的内容

XIAO nRF54L15 添加 overlay 文件并修改 conf 文件

板载按键

XIAO nRF54L15(Sense) 配备了两个重要的物理按键,它们在设备操作和固件编程中发挥着关键作用:复位按键用户按键。了解它们的功能对于日常使用和固件更新至关重要。


复位按键

复位按键用于对设备执行硬复位操作。

  • 功能:
    • 强制重启: 按下此按键会立即中断所有当前设备操作并使其重启,类似于电源循环。
    • 解决程序卡死: 当设备运行的程序崩溃、进入无限循环或变得无响应时,按下复位按键是强制其恢复正常运行状态的最快方法。
    • 不影响固件: 复位操作不会擦除或更改已编程到设备中的固件。它只是重启当前运行的应用程序。

用户按键

用户按键是一个多功能的可编程输入,在您的应用程序中提供灵活的控制。

功能:

  • 可定制输入:与复位按键的固定功能不同,用户按键的操作完全由您编程的固件定义。

  • 事件触发:它可以被编程来触发特定事件、控制不同功能,或作为应用程序的通用输入。

数字

硬件准备

Seeed Studio XIAO nRF54L15 SenseSeeed Studio XIAO 扩展底板配 Grove OLEDGrove - 继电器

软件实现


#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>


LOG_MODULE_REGISTER(main_app, CONFIG_LOG_DEFAULT_LEVEL);

static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(DT_ALIAS(sw1), gpios); // Get the button device from the device tree alias
static const struct gpio_dt_spec relay = GPIO_DT_SPEC_GET(DT_ALIAS(relay0), gpios); // Get the relay device from the device tree alias

int main(void)
{
int ret;

LOG_INF("Starting Zephyr button and relay example...");

/* Check if GPIO devices are ready */
if (!gpio_is_ready_dt(&button)) {
LOG_ERR("Button device %s is not ready", button.port->name);
return -1;
}

if (!gpio_is_ready_dt(&relay)) {
LOG_ERR("Relay device %s is not ready", relay.port->name);
return -1;
}

/* Configure button pin as input mode */
ret = gpio_pin_configure_dt(&button, GPIO_INPUT);
if (ret != 0) {
LOG_ERR("Failed to configure %s pin %d (error %d)", button.port->name, button.pin, ret);
return -1;
}

/* Configure relay pin as output mode */
ret = gpio_pin_configure_dt(&relay, GPIO_OUTPUT_ACTIVE);
if (ret != 0) {
LOG_ERR("Failed to configure %s pin %d (error %d)", relay.port->name, relay.pin, ret);
return -1;
}

LOG_INF("Press the button to toggle the relay...");

while (1) {
/* Read button state */
int button_state = gpio_pin_get_dt(&button);

/* Check if read is successful */
if (button_state < 0) {
LOG_ERR("Error reading button pin: %d", button_state);
return -1;
}

if (button_state == 0) { // Button pressed (ACTIVE_LOW)
gpio_pin_set_dt(&relay, 1); // Turn on relay (high level)
} else { // Button not pressed
gpio_pin_set_dt(&relay, 0); // Turn off relay (low level)
}

k_msleep(10); /* Short delay to avoid busy looping */
}
return 0;
}

设备树配置

static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(DT_ALIAS(sw1), gpios);

  • 这行代码利用 Zephyr 的设备树系统通过名为 sw1 的别名获取按钮的 GPIO 设备信息。这种方法将代码与特定的硬件引脚解耦,提高了可移植性。

static const struct gpio_dt_spec relay = GPIO_DT_SPEC_GET(DT_ALIAS(relay0), gpios);

  • 同样,这行代码获取名为 relay0 的继电器 GPIO 设备信息。

设备就绪检查

if (!gpio_is_ready_dt(&button))if (!gpio_is_ready_dt(&relay))

  • 在程序开始执行任何操作之前,代码检查按钮和继电器设备是否成功初始化并准备就绪。这是 Zephyr 驱动程序编程中的最佳实践,可防止设备未正确配置时程序崩溃。

引脚配置

gpio_pin_configure_dt(&button, GPIO_INPUT);

  • 这行代码将按钮的 GPIO 引脚配置为输入模式。这是读取引脚电平的必要步骤,程序将监控引脚的电压电平来确定按钮是否被按下。

gpio_pin_configure_dt(&relay, GPIO_OUTPUT_ACTIVE);

  • 这行代码将继电器的 GPIO 引脚配置为输出模式。GPIO_OUTPUT_ACTIVE 标志通常表示引脚在配置后将处于活动状态,为控制继电器做准备。

主循环逻辑

while (1): 代码进入无限循环,持续执行以下操作。

int button_state = gpio_pin_get_dt(&button);: 在每个循环中,程序读取按钮引脚的当前电平状态。

if (button_state == 0): 此逻辑检查按钮是否被按下。在许多电路设计中,按钮按下时将引脚连接到地线(GND),导致电平为 0(即低电平)。

gpio_pin_set_dt(&relay, 1);: 如果按钮状态为 0(按下),则将继电器引脚设置为 1(高电平),这会闭合继电器并打开连接到它的设备(例如灯)。

else: 如果按钮未被按下(状态为 1),执行 gpio_pin_set_dt(&relay, 0); 将继电器引脚设置为 0(低电平),这会断开继电器并关闭连接到它的设备。

k_msleep(10);: 代码在每个循环结束时添加 10 毫秒的短暂延迟,以避免 CPU 忙等等。这是一个简单的防抖处理。这是一个简单的防抖处理,可防止由于按钮的物理抖动而导致的多次触发,同时也降低功耗。

结果图

模拟

硬件准备

Seeed Studio XIAO nRF54L15 SenseGrove-可变色LEDGrove-旋转角度传感器Seeed Studio Grove Base for XIAO

软件实现


#include <zephyr/kernel.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/logging/log.h>

// Register log module
LOG_MODULE_REGISTER(pot_pwm_example, CONFIG_LOG_DEFAULT_LEVEL);

// --- ADC Configuration ---
#if !DT_NODE_EXISTS(DT_PATH(zephyr_user)) || \
!DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels)
#error "No suitable devicetree overlay specified for ADC channels"
#endif

#define DT_SPEC_AND_COMMA(node_id, prop, idx) \
ADC_DT_SPEC_GET_BY_IDX(node_id, idx),

static const struct adc_dt_spec adc_channels[] = {
DT_FOREACH_PROP_ELEM(DT_PATH(zephyr_user), io_channels, DT_SPEC_AND_COMMA)
};

// Define the index of the potentiometer ADC channel in the adc_channels array
#define POTENTIOMETER_ADC_CHANNEL_IDX 1

// --- PWM Configuration ---
// Get PWM LED device
static const struct pwm_dt_spec led = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led));

// Define PWM period as 1 millisecond (1,000,000 nanoseconds)
// This corresponds to a 1 kHz PWM frequency, suitable for LED brightness adjustment without visible flicker
#define PWM_PERIOD_NS 1000000UL

int main(void)
{
int ret;
uint16_t adc_raw_value;
int32_t adc_millivolts;

LOG_INF("Starting Zephyr Potentiometer to PWM example...");

// --- ADC initialization and setup ---
if (!adc_is_ready_dt(&adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX])) {
LOG_ERR("ADC controller device %s not ready", adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].dev->name);
return 0;
}

ret = adc_channel_setup_dt(&adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX]);
if (ret < 0) {
LOG_ERR("Could not setup ADC channel for potentiometer (%d)", ret);
return 0;
}
LOG_INF("ADC device %s, channel %d ready for potentiometer.",
adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].dev->name,
adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].channel_id);

// --- PWM initialization and setup ---
if (!device_is_ready(led.dev)) {
LOG_ERR("Error: PWM device %s is not ready", led.dev->name);
return 0;
}
LOG_INF("PWM Period for LED set to %lu ns (%.1f Hz)",
PWM_PERIOD_NS, (double)NSEC_PER_SEC / PWM_PERIOD_NS); // Use PWM_PERIOD_NS instead of led.period


// ADC sequence configuration
struct adc_sequence sequence = {
.buffer = &adc_raw_value,
.buffer_size = sizeof(adc_raw_value),
.resolution = adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].resolution,
};

// --- Main loop ---
while (1) {
(void)adc_sequence_init_dt(&adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX], &sequence);

ret = adc_read(adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].dev, &sequence);
if (ret < 0) {
LOG_ERR("Error %d: ADC read failed for channel %d",
ret, adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].channel_id);
k_msleep(100);
continue;
}

int sensor_value = adc_raw_value;

uint32_t max_adc_raw = (1U << adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX].resolution) - 1;

// --- Map ADC raw value to PWM duty cycle ---
uint32_t output_duty_ns = (PWM_PERIOD_NS * sensor_value) / max_adc_raw;

// Set PWM duty cycle
ret = pwm_set_dt(&led, PWM_PERIOD_NS, output_duty_ns);
if (ret < 0) {
LOG_ERR("Error %d: failed to set PWM duty cycle.", ret);
}

// --- Print information ---
adc_millivolts = sensor_value;
ret = adc_raw_to_millivolts_dt(&adc_channels[POTENTIOMETER_ADC_CHANNEL_IDX], &adc_millivolts);
if (ret < 0) {
LOG_WRN("ADC to mV conversion not supported/failed: %d", ret);
LOG_INF("Sensor Raw Value = %d\tOutput Duty (ns) = %u", sensor_value, output_duty_ns);
} else {
LOG_INF("Sensor Raw Value = %d (%d mV)\tOutput Duty (ns) = %u",
sensor_value, adc_millivolts, output_duty_ns);
}

k_msleep(100);
}
return 0;
}

ADC(模数转换器)和 PWM(脉宽调制)设备配置

  • pot_pwm_example 日志模块:

    • LOG_MODULE_REGISTER(pot_pwm_example, CONFIG_LOG_DEFAULT_LEVEL):这注册了一个名为 pot_pwm_example 的日志模块,并将其日志级别设置为系统的默认配置,这有助于调试。
  • ADC 配置:

    • #if !DT_NODE_EXISTS(DT_PATH(zephyr_user)) ... #endif:这个预处理器指令是一个设备树检查,确保存在包含 ADC 通道定义的有效覆盖文件。这要求用户必须为特定硬件提供正确的配置。

    • static const struct adc_dt_spec adc_channels[];:这部分代码利用 Zephyr 的设备树自动检索所有已配置 ADC 通道的信息。这种方法使代码灵活且可在不同硬件间移植,无需手动配置更改。

    • #define POTENTIOMETER_ADC_CHANNEL_IDX 1:定义了一个宏来指定电位器连接到 adc_channels 数组中的哪个通道。

  • PWM 配置:

    • static const struct pwm_dt_spec led = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led));:这行代码从设备树中检索别名 pwm_led 的 PWM 设备信息。这是 Zephyr 查找和引用硬件设备的标准做法。

    • #define PWM_PERIOD_NS 1000000UL:这定义了 PWM 信号周期为 1 毫秒(1,000,000 纳秒),对应频率为 1 kHz。这个频率非常适合 LED 调光,因为它足够高,可以防止可见的闪烁。

初始化和设置

  • 日志信息:

    • LOG_INF("Starting Zephyr Potentiometer to PWM example...");:在程序开始时打印信息日志消息,通知用户示例已开始。
  • ADC 初始化:

    • !adc_is_ready_dt():在尝试使用 ADC 设备之前,执行检查以确认设备已就绪。如果设备未就绪,记录错误并退出程序。

    • adc_channel_setup_dt():此函数配置连接到电位器的特定 ADC 通道,包括其分辨率和增益。

  • PWM 初始化:

    • !device_is_ready(led.dev):与 ADC 类似,这行代码检查 PWM 设备是否就绪。如果没有,记录错误并退出程序。

    • LOG_INF(...):打印 PWM 周期和频率信息,帮助用户确认配置。

  • ADC 序列配置:

    • struct adc_sequence sequence:定义了一个 adc_sequence 结构体来描述单个 ADC 转换操作。它指定了存储结果的缓冲区(adc_raw_value)、其大小(sizeof(adc_raw_value))以及要使用的 ADC 分辨率。

主循环 代码的核心逻辑在无限 while (1) 循环中运行:

  • ADC 读取:

    • adc_sequence_init_dt():初始化 ADC 序列以确保每次读取都使用正确的配置。

    • adc_read():这触发 ADC 转换以从电位器读取模拟值。如果读取失败,记录错误,程序暂停 100 毫秒后继续。

    • int sensor_value = adc_raw_value;:将原始 ADC 值赋给 sensor_value 变量。

  • 将 ADC 值映射到 PWM 占空比:

    • uint32_t max_adc_raw:这计算最大可能的原始 ADC 值。

    • uint32_t output_duty_ns = (PWM_PERIOD_NS * sensor_value) / max_adc_raw;:这是核心映射逻辑。它将原始 ADC 值(sensor_value)按比例缩放到 PWM 周期(PWM_PERIOD_NS)的范围,以获得调整 LED 亮度的占空比值。

  • 设置 PWM 占空比:

    • pwm_set_dt():此函数将新计算的占空比(output_duty_ns)应用到 PWM 设备,立即改变 LED 的亮度。
  • 延迟:

    • k_msleep(100):程序在每次循环后暂停 100 毫秒。这控制了 ADC 读取和 PWM 更新的频率,防止过度的 CPU 负载并提供稳定的用户体验。

结果图

UART

硬件准备

Seeed Studio XIAO nRF54L15 SenseL76K GNSS Module for Seeed Studio XIAO

软件实现


软件
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/logging/log.h>
#include <nrfx_power.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

// Register log module
LOG_MODULE_REGISTER(gps_app, LOG_LEVEL_INF);

// Type definitions
#define UBYTE uint8_t
#define UWORD uint16_t
#define UDOUBLE uint32_t

// Buffer sizes
#define SENTENCE_SIZE 100
#define BUFFSIZE 800

// NMEA Commands
#define HOT_START "$PMTK101"
#define WARM_START "$PMTK102"
#define COLD_START "$PMTK103"
#define FULL_COLD_START "$PMTK104"
#define SET_PERPETUAL_STANDBY_MODE "$PMTK161"
#define SET_PERIODIC_MODE "$PMTK225"
#define SET_NORMAL_MODE "$PMTK225,0"
#define SET_PERIODIC_BACKUP_MODE "$PMTK225,1,1000,2000"
#define SET_PERIODIC_STANDBY_MODE "$PMTK225,2,1000,2000"
#define SET_PERPETUAL_BACKUP_MODE "$PMTK225,4"
#define SET_ALWAYSLOCATE_STANDBY_MODE "$PMTK225,8"
#define SET_ALWAYSLOCATE_BACKUP_MODE "$PMTK225,9"
#define SET_POS_FIX "$PMTK220"
#define SET_POS_FIX_100MS "$PMTK220,100"
#define SET_POS_FIX_200MS "$PMTK220,200"
#define SET_POS_FIX_400MS "$PMTK220,400"
#define SET_POS_FIX_800MS "$PMTK220,800"
#define SET_POS_FIX_1S "$PMTK220,1000"
#define SET_POS_FIX_2S "$PMTK220,2000"
#define SET_POS_FIX_4S "$PMTK220,4000"
#define SET_POS_FIX_8S "$PMTK220,8000"
#define SET_POS_FIX_10S "$PMTK220,10000"
#define SET_SYNC_PPS_NMEA_OFF "$PMTK255,0"
#define SET_SYNC_PPS_NMEA_ON "$PMTK255,1"
#define SET_NMEA_BAUDRATE "$PMTK251"
#define SET_NMEA_BAUDRATE_115200 "$PMTK251,115200"
#define SET_NMEA_BAUDRATE_57600 "$PMTK251,57600"
#define SET_NMEA_BAUDRATE_38400 "$PMTK251,38400"
#define SET_NMEA_BAUDRATE_19200 "$PMTK251,19200"
#define SET_NMEA_BAUDRATE_14400 "$PMTK251,14400"
#define SET_NMEA_BAUDRATE_9600 "$PMTK251,9600"
#define SET_NMEA_BAUDRATE_4800 "$PMTK251,4800"
#define SET_REDUCTION "$PMTK314,-1"
#define SET_NMEA_OUTPUT "$PMTK314,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0"

// Struct definitions
typedef struct
{
double Lon; // GPS Longitude
double Lat; // GPS Latitude
char Lon_area; // E or W
char Lat_area; // N or S
UBYTE Time_H; // Time Hour
UBYTE Time_M; // Time Minute
UBYTE Time_S; // Time Second
UBYTE Status; // 1: Successful positioning, 0: Positioning failed
} GNRMC;

typedef struct
{
double Lon;
double Lat;
} Coordinates;

// Global variables and constants
char const Temp[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
static const double pi = 3.14159265358979324;
static const double a = 6378245.0;
static const double ee = 0.00669342162296594323;
static const double x_pi = 3.14159265358979324 * 3000.0 / 180.0;

static char buff_t[BUFFSIZE] = {0};
static GNRMC GPS;

// UART device and buffers
static const struct device *uart_dev;
static char latest_gnrmc[SENTENCE_SIZE];
static volatile bool new_gnrmc_available = false;

// Function prototypes
void DEV_Uart_SendByte(char data);
void DEV_Uart_SendString(char *data);
void L76X_Send_Command(char *data);
GNRMC L76X_Gat_GNRMC(void);
Coordinates L76X_Baidu_Coordinates(void);
Coordinates L76X_Google_Coordinates(void);
static double transformLat(double x, double y);
static double transformLon(double x, double y);
static Coordinates bd_encrypt(Coordinates gg);
static Coordinates transform(Coordinates gps);

// UART interrupt callback
static void uart_callback(const struct device *dev, void *user_data)
{
ARG_UNUSED(user_data);
static char temp_buffer[SENTENCE_SIZE];
static int temp_index = 0;

while (uart_irq_update(dev) && uart_irq_is_pending(dev))
{
if (uart_irq_rx_ready(dev))
{
uint8_t byte;
if (uart_fifo_read(dev, &byte, 1) == 1)
{
if (byte == '\n')
{
temp_buffer[temp_index] = '\0';
if (strncmp(temp_buffer, "$GNRMC", 6) == 0 || strncmp(temp_buffer, "$PNRMC", 6) == 0)
{
strncpy(latest_gnrmc, temp_buffer, SENTENCE_SIZE);
new_gnrmc_available = true;
}
temp_index = 0;
}
else
{
if (temp_index < SENTENCE_SIZE - 1)
{
temp_buffer[temp_index++] = byte;
}
else
{
temp_index = 0; // Reset on overflow
}
}
}
}
}
}

// Main function
int main(void)
{
// Request constant latency mode for power management
nrfx_power_constlat_mode_request();
LOG_INF("Starting L76X GPS Module Example");

// Initialize UART device
uart_dev = DEVICE_DT_GET(DT_NODELABEL(xiao_serial));
if (!device_is_ready(uart_dev))
{
LOG_ERR("UART device not ready!");
return -1;
}
LOG_INF("UART device initialized.");

// Configure UART interrupt
if (uart_irq_callback_user_data_set(uart_dev, uart_callback, NULL) != 0)
{
LOG_ERR("Failed to set UART callback!");
return -1;
}
uart_irq_rx_enable(uart_dev);
LOG_INF("UART interrupt enabled.");

// Initialize GPS module
L76X_Send_Command(SET_NMEA_OUTPUT);
k_msleep(100);
L76X_Send_Command(SET_POS_FIX_1S);
k_msleep(100);

LOG_INF("GPS module initialized. Waiting for data...");

while (true)
{
// Check for new GNRMC sentence
if (new_gnrmc_available)
{
strncpy(buff_t, latest_gnrmc, BUFFSIZE);
new_gnrmc_available = false;

// Log raw GNRMC sentence for debugging
LOG_INF("Raw GNRMC: %s", buff_t);

// Parse GNRMC data
GPS = L76X_Gat_GNRMC();

// Output GPS data
LOG_INF("\n--- GPS Data ---");
LOG_INF("Time (GMT+8): %02d:%02d:%02d", GPS.Time_H, GPS.Time_M, GPS.Time_S);
if (GPS.Status == 1)
{
LOG_INF("Latitude (WGS-84): %.6f %c", GPS.Lat, GPS.Lat_area);
LOG_INF("Longitude (WGS-84): %.6f %c", GPS.Lon, GPS.Lon_area);

// Coordinate conversion
Coordinates baidu_coords = L76X_Baidu_Coordinates();
LOG_INF("Baidu Latitude: %.6f", baidu_coords.Lat);
LOG_INF("Baidu Longitude: %.6f", baidu_coords.Lon);

Coordinates google_coords = L76X_Google_Coordinates();
LOG_INF("Google Latitude: %.6f", google_coords.Lat);
LOG_INF("Google Longitude: %.6f", google_coords.Lon);
LOG_INF("GPS positioning successful.");
}
else
{
LOG_INF("GPS positioning failed or no valid data.");
}
}
else
{
LOG_INF("No new GNRMC data available.");
}

k_msleep(2000); // Wait 2 seconds before next reading
}

return 0;
}

// Send a single byte
void DEV_Uart_SendByte(char data)
{
uart_poll_out(uart_dev, data);
}

// Send a string
void DEV_Uart_SendString(char *data)
{
while (*data)
{
DEV_Uart_SendByte(*data++);
}
}

// Send L76X command with checksum
void L76X_Send_Command(char *data)
{
char Check = data[1], Check_char[3] = {0};
UBYTE i = 0;
DEV_Uart_SendByte('\r');
DEV_Uart_SendByte('\n');

for (i = 2; data[i] != '\0'; i++)
{
Check ^= data[i]; // Calculate checksum
}
Check_char[0] = Temp[Check / 16 % 16];
Check_char[1] = Temp[Check % 16];
Check_char[2] = '\0';

DEV_Uart_SendString(data);
DEV_Uart_SendByte('*');
DEV_Uart_SendString(Check_char);
DEV_Uart_SendByte('\r');
DEV_Uart_SendByte('\n');
}

// Parse GNRMC data
GNRMC L76X_Gat_GNRMC(void)
{
GNRMC gps = {0}; // Initialize with zeros
UWORD add = 0, x = 0, z = 0, i = 0;
UDOUBLE Time = 0;

add = 0;
while (add < BUFFSIZE)
{
// Look for GNRMC or PNRMC sentence
if (buff_t[add] == '$' && buff_t[add + 1] == 'G' && (buff_t[add + 2] == 'N' || buff_t[add + 2] == 'P') &&
buff_t[add + 3] == 'R' && buff_t[add + 4] == 'M' && buff_t[add + 5] == 'C')
{
x = 0;
for (z = 0; x < 12; z++)
{
if (buff_t[add + z] == '\0')
{
break;
}
if (buff_t[add + z] == ',')
{
x++;
if (x == 1)
{ // Time field
if (buff_t[add + z + 1] != ',')
{ // Check if time field is not empty
Time = 0;
for (i = 0; buff_t[add + z + i + 1] != '.'; i++)
{
if (buff_t[add + z + i + 1] == '\0' || buff_t[add + z + i + 1] == ',')
{
break;
}
Time = (buff_t[add + z + i + 1] - '0') + Time * 10;
}
gps.Time_H = Time / 10000 + 8; // Adjust for GMT+8
gps.Time_M = (Time / 100) % 100;
gps.Time_S = Time % 100;
if (gps.Time_H >= 24)
{
gps.Time_H = gps.Time_H - 24;
}
}
}
else if (x == 2)
{ // Status field
if (buff_t[add + z + 1] == 'A')
{
gps.Status = 1; // Position successful
}
else
{
gps.Status = 0; // Positioning failed
break; // Exit early if invalid
}
}
else if (x == 3)
{ // Latitude field
if (buff_t[add + z + 1] != ',')
{ // Check if latitude field is not empty
double latitude_val = 0;
UBYTE decimal_found = 0;
double decimal_multiplier = 0.1;

int k = 1;
while (buff_t[add + z + k] != ',' && buff_t[add + z + k] != '\0')
{
if (buff_t[add + z + k] == '.')
{
decimal_found = 1;
k++;
continue;
}
if (!decimal_found)
{
latitude_val = latitude_val * 10 + (buff_t[add + z + k] - '0');
}
else
{
latitude_val = latitude_val + (buff_t[add + z + k] - '0') * decimal_multiplier;
decimal_multiplier *= 0.1;
}
k++;
}
gps.Lat = latitude_val;
gps.Lat_area = buff_t[add + z + k + 1]; // N or S
z += k + 1;
}
else
{
gps.Status = 0; // Invalid data
break;
}
}
else if (x == 5)
{ // Longitude field
if (buff_t[add + z + 1] != ',')
{ // Check if longitude field is not empty
double longitude_val = 0;
UBYTE decimal_found = 0;
double decimal_multiplier = 0.1;

int k = 1;
while (buff_t[add + z + k] != ',' && buff_t[add + z + k] != '\0')
{
if (buff_t[add + z + k] == '.')
{
decimal_found = 1;
k++;
continue;
}
if (!decimal_found)
{
longitude_val = longitude_val * 10 + (buff_t[add + z + k] - '0');
}
else
{
longitude_val = longitude_val + (buff_t[add + z + k] - '0') * decimal_multiplier;
decimal_multiplier *= 0.1;
}
k++;
}
gps.Lon = longitude_val;
gps.Lon_area = buff_t[add + z + k + 1]; // E or W
z += k + 1;
break;
}
else
{
gps.Status = 0; // Invalid data
break;
}
}
}
}
break;
}
add++;
}
return gps;
}

// Convert to Baidu coordinates (BD-09)
Coordinates L76X_Baidu_Coordinates(void)
{
Coordinates wgs84_coords;
wgs84_coords.Lat = GPS.Lat;
wgs84_coords.Lon = GPS.Lon;

Coordinates gcj02_coords = transform(wgs84_coords);
Coordinates bd09_coords = bd_encrypt(gcj02_coords);
return bd09_coords;
}

// Convert to Google coordinates (GCJ-02)
Coordinates L76X_Google_Coordinates(void)
{
Coordinates wgs84_coords;
wgs84_coords.Lat = GPS.Lat;
wgs84_coords.Lon = GPS.Lon;

Coordinates gcj02_coords = transform(wgs84_coords);
return gcj02_coords;
}

// Coordinate transformation helper functions
static double transformLat(double x, double y)
{
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(fabs(x));
ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * sin(y * pi) + 40.0 * sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * sin(y / 12.0 * pi) + 320 * sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}

static double transformLon(double x, double y)
{
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(fabs(x));
ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * sin(x * pi) + 40.0 * sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * sin(x / 12.0 * pi) + 300.0 * sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}

static Coordinates bd_encrypt(Coordinates gg)
{
Coordinates bd;
double x = gg.Lon, y = gg.Lat;
double z = sqrt(x * x + y * y) + 0.00002 * sin(y * x_pi);
double theta = atan2(y, x) + 0.000003 * cos(x * x_pi);
bd.Lon = z * cos(theta) + 0.0065;
bd.Lat = z * sin(theta) + 0.006;
return bd;
}

static Coordinates transform(Coordinates gps)
{
Coordinates gg;
double dLat = transformLat(gps.Lon - 105.0, gps.Lat - 35.0);
double dLon = transformLon(gps.Lon - 105.0, gps.Lat - 35.0);
double radLat = gps.Lat / 180.0 * pi;
double magic = sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * pi);
gg.Lat = gps.Lat + dLat;
gg.Lon = gps.Lon + dLon;
return gg;
}

GPS 模块配置和初始化

  • gps_app 日志模块:

-LOG_MODULE_REGISTER(gps_app, LOG_LEVEL_INF):这注册了一个名为 gps_app 的日志模块,并将其日志级别设置为 INFO。这允许程序通过 Zephyr 的日志系统输出信息,这对调试和监控很有用。

  • 类型定义和宏:

-UBYTEUWORDUDOUBLE:这些是自定义的无符号整数类型别名,通过明确变量的预期大小来提高代码可读性。

  • SENTENCE_SIZE, BUFFSIZE:这些定义了用于存储 NMEA 语句和更大数据缓冲区的固定大小。

  • 宏如 HOT_START, SET_NMEA_OUTPUT:这些宏定义了发送到 L76X GPS 模块的各种 NMEA 协议命令,用于配置其操作模式、输出频率、波特率等。

  • 结构体定义:

    • GNRMC:此结构体用于存储从 GNRMC(GPS 推荐最小特定数据)NMEA 语句解析的关键信息,包括经度、纬度、时间、状态和基本方向。

    • Coordinates:一个简单的结构体,用于存储地理坐标的经度和纬度。

  • 全局变量和常量:

    • buff_t:一个大小为 BUFFSIZE 的全局缓冲区,用于存储原始 UART 数据。

-GPS:一个全局 GNRMC 结构体实例,用于保存解析的 GPS 数据。

  • uart_dev:指向 UART 设备结构体的指针,用于 UART 通信。

  • new_gnrmc_available:一个易失性布尔标志,当接收到新的有效 GNRMC 语句时设置为 true,通知主循环有新数据可供处理。

  • uart_callback() 函数:

    • 这是一个 UART 中断回调函数,当 UART 接收到数据时触发。

    • 该函数逐字节读取 UART FIFO,当遇到换行符 \n 时将数据作为完整语句处理。

主函数 main()

  • 系统初始化:

    • nrfx_power_constlat_mode_request():请求恒定延迟模式,确保电源管理不会干扰实时操作。

    • uart_dev = DEVICE_DT_GET:获取 UART 设备句柄,并使用 device_is_ready() 检查设备是否就绪。

    • uart_irq_callback_user_data_set()uart_irq_rx_enable():这些配置并启用 UART 接收中断,注册 uart_callback 函数作为中断处理程序,确保异步接收 GPS 数据。

  • GPS 模块初始化:

    • L76X_Send_Command(SET_NMEA_OUTPUT):发送命令配置 GPS 模块仅输出指定的 NMEA 语句如 GNRMC,减少不必要的数据流量。

-L76X_Send_Command(SET_POS_FIX_1S):将 GPS 模块的位置更新频率设置为 1 秒。

  • 主循环:

    • 循环无限运行,持续检查 new_gnrmc_available 标志。

    • 如果标志为 true,它将最新的 GPS 语句从 latest_gnrmc 复制到 buff_t,然后调用 L76X_Gat_GNRMC() 解析数据。

    • 根据解析结果,它打印时间、WGS-84 经纬度以及转换后的百度和谷歌坐标。

    • 如果 GPS.Status 为 0,它打印"定位失败"消息。

    • 如果没有新数据可用,它打印"没有新的 GNRMC 数据可用"。

    • k_msleep(2000):程序在每次循环后暂停 2 秒,以控制输出频率。

结果图

I2C

硬件准备

Seeed Studio XIAO nRF54L15 SenseSeeed Studio XIAO 扩展底板

软件实现


#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/display/cfb.h>
#include <stdio.h>
#include <string.h>

#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main_app, LOG_LEVEL);

/**
* @brief Initializes the display device.
* @param[out] dev Pointer to the display device struct.
* @return 0 on success, -1 on failure.
*/
static int display_init(const struct device **dev) {
*dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(*dev)) {
LOG_ERR("Device %s not ready", (*dev)->name);
return -1;
}

if (display_set_pixel_format(*dev, PIXEL_FORMAT_MONO10) != 0) {
if (display_set_pixel_format(*dev, PIXEL_FORMAT_MONO01) != 0) {
LOG_ERR("Failed to set required pixel format");
return -1;
}
}

LOG_INF("Initialized %s", (*dev)->name);
return 0;
}

/**
* @brief Initializes the Compact Framebuffer (CFB) and display blanking.
* @param dev Pointer to the display device struct.
* @return 0 on success, -1 on failure.
*/
static int framebuffer_setup(const struct device *dev) {
if (cfb_framebuffer_init(dev)) {
LOG_ERR("Framebuffer initialization failed!");
return -1;
}
cfb_framebuffer_clear(dev, true);
display_blanking_off(dev);
return 0;
}

/**
* @brief Selects a suitable font for the display.
* @param dev Pointer to the display device struct.
* @param[out] font_width Pointer to store the width of the selected font.
* @param[out] font_height Pointer to store the height of the selected font.
* @return 0 on success, -1 if no suitable font is found.
*/
static int select_font(const struct device *dev, uint8_t *font_width, uint8_t *font_height) {
int chosen_font_idx = -1;
uint8_t current_font_width, current_font_height;

for (int idx = 0; idx < 42; idx++) {
if (cfb_get_font_size(dev, idx, &current_font_width, &current_font_height) == 0) {
if (current_font_width == 8 && current_font_height == 8) {
chosen_font_idx = idx;
*font_width = current_font_width;
*font_height = current_font_height;
cfb_framebuffer_set_font(dev, chosen_font_idx);
LOG_INF("Selected font idx: %d, width: %d, height: %d", chosen_font_idx, *font_width, *font_height);
break;
}
if (chosen_font_idx == -1 && current_font_width > 0 && current_font_height > 0) {
chosen_font_idx = idx;
*font_width = current_font_width;
*font_height = current_font_height;
cfb_framebuffer_set_font(dev, chosen_font_idx);
LOG_INF("Defaulting to font idx: %d, width: %d, height: %d", chosen_font_idx, *font_width, *font_height);
}
} else {
break;
}
}

if (chosen_font_idx == -1) {
LOG_ERR("No suitable font found or loaded!");
return -1;
}
return 0;
}

/**
* @brief Prints a single line of text at specified row and column.
* @param dev Pointer to the display device struct.
* @param text The string to print.
* @param row The row number (0-indexed) where the text should start.
* @param col The column number (0-indexed) where the text should start.
* @param font_width The width of the currently selected font in pixels.
* @param font_height The height of the currently selected font in pixels.
*/
static void print_text_by_row_col(const struct device *dev, const char *text, int row, int col,
uint8_t font_width, uint8_t font_height) {
int pixel_x = col * font_width;
int pixel_y = row * font_height;

if (cfb_print(dev, text, pixel_x, pixel_y)) {
LOG_ERR("Failed to print text: \"%s\" at row %d, col %d", text, row, col);
}
}

int main(void) {
const struct device *dev;
uint8_t font_width = 0;
uint8_t font_height = 0;
uint16_t x_res, y_res;

if (display_init(&dev) != 0) {
return 0;
}

if (framebuffer_setup(dev) != 0) {
return 0;
}

if (select_font(dev, &font_width, &font_height) != 0) {
return 0;
}

x_res = cfb_get_display_parameter(dev, CFB_DISPLAY_WIDTH);
y_res = cfb_get_display_parameter(dev, CFB_DISPLAY_HEIGHT);
LOG_INF("Display resolution: %dx%d", x_res, y_res);
cfb_set_kerning(dev, 0);

while (1) {
cfb_framebuffer_clear(dev, false);

const char *line1_text = "nRF54L15";
// Print line1 at row 1, column 2
print_text_by_row_col(dev, line1_text, 1, 2, font_width, font_height);

const char *line2_text = "Hello World";
// Print line2 at row 2, column 1
print_text_by_row_col(dev, line2_text, 2, 1, font_width, font_height);

cfb_framebuffer_finalize(dev);
k_sleep(K_MSEC(1000));
}

return 0;
}

显示设备配置和初始化

  • main_app 日志模块:

    • #define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL 和 LOG_MODULE_REGISTER(main_app, LOG_LEVEL) 注册一个名为 main_app 的日志模块,并将其日志级别设置为系统的默认配置。这使开发者能够通过 Zephyr 的日志系统轻松调试和输出信息。
  • display_init() 函数:

    • *dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));: 这行代码从 Zephyr 设备树中检索选定的显示设备。这种方法确保代码与硬件无关。

    • display_set_pixel_format(*dev, PIXEL_FORMAT_MONO10):代码尝试将显示器的像素格式设置为 PIXEL_FORMAT_MONO10。如果失败,则尝试 PIXEL_FORMAT_MONO01。这确保显示器在单色模式下运行,这对某些显示技术(例如 OLED 或电子纸)是必需的。

  • framebuffer_setup() 函数:

    • cfb_framebuffer_init(dev):这初始化紧凑帧缓冲区(CFB)。CFB 是 Zephyr 中的轻量级图形库,用于在显示器上绘制文本和简单图形。

    • cfb_framebuffer_clear(dev, true):这清除帧缓冲区并立即将其内容写入显示器,确保屏幕清洁。

    • display_blanking_off(dev):这关闭显示器的消隐功能,通常表示显示器已准备好接收数据并显示图像。

  • select_font() 函数:

    • cfb_get_font_size():此函数循环遍历可用字体以找到合适的字体。

    • 代码优先选择 8x8 像素字体,因为它是常见且易于阅读的小字体。

    • 如果找不到 8x8 字体,则选择第一个可用的非零大小字体作为备选。

    • cfb_framebuffer_set_font(dev, chosen_font_idx): 找到合适的字体后,将其设置为帧缓冲区的当前字体。

  • print_text_by_row_col() 函数:

-int pixel_x = col * font_width;int pixel_y = row * font_height;: 此函数将文本的行列坐标(以字符为单位)转换为像素坐标,使文本定位更加直观。

  • cfb_print(): 这是 CFB 库的核心函数,用于在指定像素位置打印文本。

主循环 代码的核心逻辑在无限 while (1)循环中运行:

  • 清除屏幕:cfb_framebuffer_clear(dev, false): 在每个循环开始时,这清除帧缓冲区而不立即刷新显示器。这允许一次绘制多个元素,防止屏幕闪烁。

  • 打印文本:

    • 定义了两个字符串,line1_textline2_text

    • print_text_by_row_col():使用自定义函数在屏幕上的指定行列位置打印这两行文本。第一行在 (1, 2) 位置打印,第二行在 (2, 1) 位置打印。

    • 刷新显示器:cfb_framebuffer_finalize(dev): 此函数一次性将所有待处理的绘制命令从帧缓冲区发送到显示器,使所有内容同时出现。

    • 延迟:k_sleep(K_MSEC(1000)): 每个循环后,程序暂停 1000 毫秒(1 秒)。这控制屏幕更新频率,适用于以稳定方式显示静态信息的应用程序,如时钟或传感器数据。

结果图

SPI

硬件准备

Seeed Studio XIAO nRF54L15 SenseePaper Driver Board for Seeed Studio XIAO

软件实现


#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <lvgl.h>

#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(epaper_simple);

int main(void)
{
// Get display device
const struct device *display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
LOG_ERR("Display device not ready!");
return 0;
}
LOG_INF("Display device ready.");

// Initialize LVGL
// Must be called before any LVGL object creation or operation
lv_init();

// Turn off display blanking (for ePaper, this usually triggers a full refresh to clear old content)
if (display_blanking_off(display_dev)) {
LOG_ERR("Failed to turn off display blanking!");
return 0;
}
LOG_INF("Display blanking is off. Screen should be cleared by full refresh.");

// Get the current active screen and set its background to white
// This is also an LVGL-level "clear" operation to ensure the canvas is white
lv_obj_t *scr = lv_scr_act();
lv_obj_set_style_bg_color(scr, lv_color_white(), LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, LV_STATE_DEFAULT);

// Remove screen padding and scrollbar
lv_obj_set_style_pad_all(scr, 0, LV_STATE_DEFAULT);
lv_obj_set_scrollbar_mode(scr, LV_SCROLLBAR_MODE_OFF);


// Get display width and height (for layout)
lv_disp_t *disp = lv_disp_get_default();
lv_coord_t width = lv_disp_get_hor_res(disp);
lv_coord_t height = lv_disp_get_ver_res(disp);
LOG_INF("Display width: %d, height: %d", width, height);


// Create a centered panel
lv_obj_t *panel = lv_obj_create(scr);
lv_obj_set_size(panel, 300, 100);
lv_obj_align(panel, LV_ALIGN_CENTER, 0, 0);

// Set panel background to white, border to black for visibility
lv_obj_set_style_bg_color(panel, lv_color_white(), LV_STATE_DEFAULT);
lv_obj_set_style_border_color(panel, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_border_width(panel, 2, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(panel, 10, LV_STATE_DEFAULT);


// Add text to the panel
lv_obj_t *label = lv_label_create(panel);
lv_label_set_text(label, "HELLO EPAPER");

// Set text color to black for visibility on white background
lv_obj_set_style_text_color(label, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_text_font(label, &lv_font_montserrat_24, LV_STATE_DEFAULT);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

// Add a time label at the top right
lv_obj_t *time_label = lv_label_create(scr);
lv_label_set_text(time_label, "Time 07:21 PM");
lv_obj_set_style_text_color(time_label, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_text_font(time_label, &lv_font_montserrat_18, LV_STATE_DEFAULT);
lv_obj_align(time_label, LV_ALIGN_TOP_RIGHT, -20, 10);

// Add a Zephyr logo at the top left
lv_obj_t *zephyr_label = lv_label_create(scr);
lv_label_set_text(zephyr_label, "Powered by Zephyr");
lv_obj_set_style_text_color(zephyr_label, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_text_font(zephyr_label, &lv_font_montserrat_24, LV_STATE_DEFAULT);
lv_obj_align(zephyr_label, LV_ALIGN_BOTTOM_LEFT, 20, -10);

// Add author label at the bottom right
lv_obj_t *author_label = lv_label_create(scr);
lv_label_set_text(author_label, "Author: Stellar");
lv_obj_set_style_text_color(author_label, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_text_font(author_label, &lv_font_montserrat_16, LV_STATE_DEFAULT);
lv_obj_align(author_label, LV_ALIGN_BOTTOM_RIGHT, -20, -10);


// Add four squares at the top left with a for loop
lv_obj_t *squares[4];
int square_offsets = 20;
for (int i = 0; i < 4; i++) {
squares[i] = lv_obj_create(scr);
lv_obj_set_size(squares[i], 30, 30);
lv_obj_set_style_bg_color(squares[i], lv_color_white(), LV_STATE_DEFAULT);
lv_obj_set_style_border_color(squares[i], lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_border_width(squares[i], 2, LV_STATE_DEFAULT);
lv_obj_set_style_radius(squares[i], 0, LV_STATE_DEFAULT);
lv_obj_align(squares[i], LV_ALIGN_TOP_LEFT, square_offsets, 20);
square_offsets+=40;
}

while (1) {
lv_task_handler();
k_sleep(K_MSEC(1000)); // Lower refresh rate, suitable for ePaper
}
return 0;
}

设备初始化:

  • 代码首先使用 DEVICE_DT_GET(DT_CHOSEN(zephyr_display)) 从设备树中获取显示设备。

  • 然后调用 device_is_ready() 检查设备是否已正确初始化并准备好进行操作。这是任何硬件交互的关键第一步。

LVGL 初始化:

  • lv_init() 是 LVGL 图形库的入口点。必须在创建任何 LVGL 对象或执行任何操作之前调用它,因为它会初始化库的内部状态。

屏幕清除:

  • 调用 display_blanking_off() 函数。对于电子纸显示器,这通常会触发完全刷新以清除屏幕上的任何旧内容。

  • 为了进一步确保画布干净,代码使用 lv_scr_act() 获取当前活动屏幕,并使用 lv_obj_set_style_bg_color() 将其背景颜色设置为白色,覆盖整个显示区域。

屏幕布局准备:

  • 使用函数 lv_disp_get_hor_res()lv_disp_get_ver_res() 获取显示器的实际宽度和高度,这有助于后续精确放置UI元素。

  • 代码还移除了屏幕的内边距 (lv_obj_set_style_pad_all()) 和滚动条 (lv_obj_set_scrollbar_mode()) 以最大化可用的绘制区域。

UI元素创建和配置:

  • 面板:使用 lv_obj_create(scr) 创建面板对象。使用 lv_obj_set_size()lv_obj_align() 设置其大小和居中对齐。使用 lv_obj_set_style_bg_color()lv_obj_set_style_border_color() 等函数配置其样式,包括白色背景和黑色边框。

  • 标签:

    • 使用 lv_label_create() 创建文本标签。

    • lv_label_set_text() 设置标签的文本内容。

    • 使用 lv_obj_set_style_text_color() 和 lv_obj_set_style_text_font() 设置文本颜色和字体大小。

  • lv_obj_align() 函数将每个标签放置在屏幕上的特定位置,如中心、右上角、左下角和右下角。

方块:使用 for 循环创建四个小方块对象。依次设置它们的大小、样式(白色填充和黑色边框)和位置,将它们水平排列在屏幕左上角。

主循环:

  • while(1) 循环是程序的连续执行部分。

  • 在循环中持续调用 lv_task_handler() 来处理所有 LVGL 内部任务,如更新UI元素和处理事件。

  • k_sleep(K_MSEC(1000)) 暂停线程1000毫秒。对于静态显示

结果图

技术支持与产品讨论

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

Loading Comments...