在嵌入式开发中,一套高效、直观的数据监控系统能极大提升调试与运维效率。本文分享的是基于恩智浦 FRDM-MCXN947 开发板的 USB Monitor 项目实践:通过 RT-Thread 操作系统、LVGL 图形库和 CherryUSB 协议栈,把 PC 端的 CPU、GPU 等实时信息采集后,显示在 240×320 的并口屏幕上。

环境搭建
环境变量配置
为了提高编译速度,这里选择在 Linux 系统下开发。在 Linux 上开发 MCXN947 需要先安装 env 工具:https://github.com/RT-Thread/env ,按照说明文档安装即可,然后配置环境变量如下。
其中 /opt/arm-gnu-toolchain-13.2.Rel1-x86_64-arm-none-eabi/bin 是编译工具链路径,/home/book/rt-thread 是 RT-Thread 根目录路径。
source ~/.env/env.sh
export RTT_CC=gcc
export RTT_ROOT=/home/book/rt-thread
export RTT_DIR=/home/book/rt-thread
export RTT_EXEC_PATH=/opt/arm-gnu-toolchain-13.2.Rel1-x86_64-arm-none-eabi/bin
export PATH=$PATH:$RTT_EXEC_PATH
如果需要将 N947 的例程从 RT-Thread 根目录中独立出来,需要删除工程中 Kconfig 文件的这行代码:
# RTT_DIR := .../.../.../...

代码高亮
这里使用 VSCode 中的 Clang 插件。代码高亮和补全可以通过编译时生成的 compile_commands.json 实现。RT-Thread 工程采用 scons 工具,所以可以使用 scons_compiledb 这个 Python 包生成 compile_commands.json,从而实现代码高亮。修改过的 SConstruct 文件如下:
import os
import sys
import rtconfig
import scons_compiledb
if os.getenv('RTT_ROOT'):
RTT_ROOT = os.getenv('RTT_ROOT')
else:
RTT_ROOT = os.path.normpath(os.getcwd() + '/../../../../..')
sys.path = sys.path + [os.path.join(RTT_ROOT, 'tools')]
try:
from building import *
except:
print('Cannot found RT-Thread root directory, please check RTT_ROOT')
print(RTT_ROOT)
exit(-1)
TARGET = 'rtthread.' + rtconfig.TARGET_EXT
if rtconfig.PLATFORM == 'armcc':
env = Environment(tools = ['mingw'],
AS = rtconfig.AS, ASFLAGS = rtconfig.AFLAGS,
CC = rtconfig.CC, CFLAGS = rtconfig.CFLAGS,
CXX = rtconfig.CXX, CXXFLAGS = rtconfig.CXXFLAGS,
AR = rtconfig.AR, ARFLAGS = '-rc',
LINK = rtconfig.LINK, LINKFLAGS = rtconfig.LFLAGS,
# overwrite cflags, because cflags has '--C99'
CXXCOM = '$CXX -o $TARGET --cpp -c $CXXFLAGS $_CCCOMCOM $SOURCES')
else:
env = Environment(tools = ['mingw'],
AS = rtconfig.AS, ASFLAGS = rtconfig.AFLAGS,
CC = rtconfig.CC, CFLAGS = rtconfig.CFLAGS,
CXX = rtconfig.CXX, CXXFLAGS = rtconfig.CXXFLAGS,
AR = rtconfig.AR, ARFLAGS = '-rc',
LINK = rtconfig.LINK, LINKFLAGS = rtconfig.LFLAGS,
CXXCOM = '$CXX -o $TARGET -c $CXXFLAGS $_CCCOMCOM $SOURCES')
env.PrependENVPath('PATH', rtconfig.EXEC_PATH)
scons_compiledb.enable(env)
env.CompileDb()
if rtconfig.PLATFORM in ['iccarm']:
env.Replace(CCCOM = ['$CC $CFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS -o $TARGET $SOURCES'])
env.Replace(ARFLAGS = [''])
env.Replace(LINKCOM = env["LINKCOM"] + ' --map rtthread.map')
Export('RTT_ROOT')
Export('rtconfig')
SDK_ROOT = os.path.abspath('./')
if os.path.exists(SDK_ROOT + '/Libraries'):
libraries_path_prefix = SDK_ROOT + '/Libraries'
else:
libraries_path_prefix = os.path.dirname(SDK_ROOT) + '/Libraries'
SDK_LIB = libraries_path_prefix
Export('SDK_LIB')
# prepare building environment
objs = PrepareBuilding(env, RTT_ROOT, has_libcpu=False)
objs.extend(SConscript(os.path.join(libraries_path_prefix, 'drivers', 'SConscript')))
# include cmsis
objs.extend(SConscript(os.path.join(libraries_path_prefix, rtconfig.BSP_LIBRARY_TYPE, 'SConscript')))
# make a building
DoBuilding(TARGET, objs)
最终搭建完成的效果如下,代码高亮后查看代码会方便很多。

LVGL 适配
屏幕扩展板
FRDM-MCXN947 预留了一个 FlexIO 接口,可以适配 8080 并口屏。于是做了一块屏幕扩展板,把手里闲置的屏幕利用起来。

实物如下,触摸排线座的位置有点偏下,不过不影响功能。

屏幕手册说明分辨率为 240×320,驱动芯片是 ST7789V,触摸芯片是 FT6336G。官方 SDK 中有 ST7796 和 FT5406 的驱动代码,后续还需要稍作修改。

驱动适配
官方 SDK 中有 ST7796 和 FT5406 的驱动程序,直接移植过来即可。同时也把 EDMA 和 SMARTDMA 的驱动复制过来,修改一下屏幕初始化序列,就可以驱动屏幕。

LVGL 适配
将 SDK 中的 lvgl_support 复制到工程中,修改屏幕宽高为 240×320。

然后在 board 中新建一个 lv_conf.h 文件,填入 LVGL 的相关配置。因为许多配置已经在 menuconfig 中设置,所以这里的配置项并不多。
#ifndef LV_CONF_H
#define LV_CONF_H
#include <rtconfig.h>
#define LV_USE_SYSMON 1
#define LV_USE_PERF_MONITOR 0
#define LV_COLOR_DEPTH 16
#define LV_HOR_RES_MAX 240
#define LV_VER_RES_MAX 320
#define LV_COLOR_16_SWAP 0
#define BSP_USING_LVGL_BENCHMARK_DEMO
#define BSP_USING_LVGL_WIDGETS_DEMO
#ifdef BSP_USING_LVGL_DAVE2D
#define LV_USE_DRAW_DAVE2D 1
#endif
#ifdef BSP_USING_LVGL_WIDGETS_DEMO
#define LV_USE_DEMO_WIDGETS 1
#define LV_DEMO_WIDGETS_SLIDESHOW 0
#endif /* BSP_USING_LVGL_WIDGETS_DEMO */
/*Benchmark your system*/
#ifdef BSP_USING_LVGL_BENCHMARK_DEMO
#define LV_USE_DEMO_BENCHMARK 1
/*Use RGB565A8 images with 16 bit color depth instead of ARGB8565*/
#define LV_DEMO_BENCHMARK_RGB565A8 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_24 1
#endif /* BSP_USING_LVGL_BENCHMARK_DEMO */
/*Stress test for LVGL*/
#ifdef BSP_USING_LVGL_STRESS_DEMO
#define LV_USE_DEMO_STRESS 1
#endif /* BSP_USING_LVGL_STRESS_DEMO */
/*Render test for LVGL*/
#ifdef BSP_USING_LVGL_RENDER_DEMO
#define LV_USE_DEMO_RENDER 1
#endif /* BSP_USING_LVGL_RENDER_DEMO */
/*Music player demo*/
#ifdef BSP_USING_LVGL_MUSIC_DEMO
#define LV_USE_DEMO_MUSIC 1
#define LV_DEMO_MUSIC_SQUARE 1
#define LV_DEMO_MUSIC_LANDSCAPE 0
#define LV_DEMO_MUSIC_ROUND 0
#define LV_DEMO_MUSIC_LARGE 0
#define LV_DEMO_MUSIC_AUTO_PLAY 0
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_16 1
#endif /* BSP_USING_LVGL_MUSIC_DEMO */
#endif
复制过来的 lvgl_support 中有对 FreeRTOS 的支持,这里将 FreeRTOS 的 API 修改为 RT-Thread 的 API。例如下面这段代码:
static void DEMO_DbIMemoryDoneCallback(status_t status, void *userData)
{
#if defined(SDK_OS_FREE_RTOS)
rt_sem_release(s_memWriteDone);
// BaseType_t taskAwake = pdFALSE;
// xSemaphoreGiveFromISR(s_memWriteDone, &taskAwake);
// portYIELD_FROM_ISR(taskAwake);
#else
s_memWriteDone = true;
#endif
}

N947 的驱动程序有 EDMA + FlexIO 和 SMARTDMA + FlexIO 两种驱动方式,具体区别不太了解。不过可以运行 LVGL 的 Benchmark 测试看下结果:左边是 SMARTDMA 的结果,右边是 EDMA 的结果。前者的 FPS 更高,所以后续继续采用 SMARTDMA + FlexIO 的驱动方式。

界面设计
使用 GUI Guider 设计界面,生成绘制界面的代码,然后添加到工程中。

还需要修改工程文件夹中的 rtconfig.py,增加一个 LV_LVGL_H_INCLUDE_SIMPLE 预定义,因为生成的代码默认包含 lvgl.h 时使用的是 #include "lvgl/lvgl.h"。
CFLAGS = DEVICE + ' -Wall -D__FPU_PRESENT -DLV_LVGL_H_INCLUDE_SIMPLE'
最终适配完成的 LVGL 代码和 GUI Guider 代码目录如下,LVGL 的 UI 绘制代码段如图右边所示,具体代码可见开源地址。

USB 通讯
适配 CDC
下位机界面初始化完成后,接下来就是把数据采集并发送给下位机更新界面。先完成 USB 通讯。这里使用的是 RT-Thread 官方推荐的 CherryUSB 开源 USB 协议栈。

将下面链接中的适配代码复制到工程中:
https://github.com/CherryUSB/cherryusb_mcx
因为传输数据比较单一,这里采用串口屏思路,直接用 CDC_ACM 通讯方式:在上位机中显示为一个 USB 转串口设备,直接使用串口 API 完成通讯。
将 RT-Thread 根目录中 rt-thread/components/drivers/usb/cherryusb/demo 文件夹里的 CDC_ACM 例程复制到工程中,并把根目录中的这两行代码屏蔽。

修改工程中的 cherryusb_port.c 文件,添加对 CDC_ACM 的支持:
/*
* Copyright (c) 2006-2024, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2024/04/23 sakumisu first version
*/
#include <rtthread.h>
#include <board.h>
/* low level init here, this has implemented in cherryusb */
/* low level deinit here, this has implemented in cherryusb */
#ifdef RT_CHERRYUSB_DEVICE_TEMPLATE_CDC_ACM
int cherryusb_devinit(void)
{
// extern void cherryusb_devinit(uint8_t busid, uintptr_t reg_base);
extern void cdc_acm_init(uint8_t busid, uintptr_t reg_base);
cdc_acm_init(0, USBHS1__USBC_BASE);
return 0;
}
INIT_COMPONENT_EXPORT(cherryusb_devinit);
#endif
#ifdef RT_CHERRYUSB_DEVICE_TEMPLATE_MSC
int cherryusb_devinit(void)
{
extern void msc_ram_init(uint8_t busid, uintptr_t reg_base);
msc_ram_init(0, USBHS1__USBC_BASE);
return 0;
}
INIT_COMPONENT_EXPORT(cherryusb_devinit);
#endif
#ifdef RT_CHERRYUSB_HOST
#include "usbh_core.h"
int cherryusb_hostinit(void)
{
usbh_initialize(0, USBHS1__USBC_BASE);
return 0;
}
INIT_COMPONENT_EXPORT(cherryusb_hostinit);
#endif
将刚才复制到工程中的 CDC_ACM demo 程序的端点收发逻辑做如下修改,增加输入信息回显:
void usbd_cdc_acm_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes)
{
USB_LOG_RAW("actual out len:%d\r\n", nbytes);
/* setup next out ep read transfer */
usbd_ep_start_read(busid, CDC_OUT_EP, read_buffer, 2048);
for (int i = 0; i < nbytes; i++) {
printf("%02x ", read_buffer[i]);
}
printf("\r\n");
}
验证
插上开发板的 USB HS 接口,用串口工具发一个数据包。

可以看到,设备已经被识别为 USB 串行设备,PID 和 VID 也是自己设置的 0xE6E9 和 0x1122。后续上位机与开发板建立通讯、锁定 COM 号,就是依靠 PID/VID 查询实现。使用串口工具给开发板发送的数据也能正常接收。
上位机
时间原因,上位机做得比较简单,实现了以下几个功能:
- 读取电脑的 CPU、GPU 占用率和温度信息,获取当前时间
- 根据 VID、PID 查询 COM,与开发板通讯,下发采集数据与时间
- 增加帧头后发送到下位机,固定长度 32+2 个字节

下位机数据更新
在开发板端增加一个线程,负责把 USB 接收到的数据更新到屏幕上,使用 LVGL 的 API 直接修改数据即可。
数据结构体:
typedef struct
{
uint16_t cpu_usage;
uint16_t mem_usage;
uint16_t gpu_usage;
uint16_t cpu_freq;
uint16_t cpu_temperature;
uint16_t gpu_temperature;
uint16_t board_temperature;
} monitor_info_u16_t;
typedef struct {
uint16_t wYear;
uint16_t wMonth;
uint16_t wDayOfWeek;
uint16_t wDay;
uint16_t wHour;
uint16_t wMinute;
uint16_t wSecond;
uint16_t wMilliseconds;
} mytime_t;
在 USB 端点输出的回调函数中,增加消息队列发送函数:
void usbd_cdc_acm_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes)
{
USB_LOG_RAW("actual out len:%d\r\n", nbytes);
/* setup next out ep read transfer */
usbd_ep_start_read(busid, CDC_OUT_EP, read_buffer, 2048);
for (int i = 0; i < nbytes; i++) {
printf("%02x ", read_buffer[i]);
}
printf("\r\n");
if (34 == nbytes)
{
rt_mq_send(&usb_mq, read_buffer, 34);
}
}
main 函数中接收消息队列:
uint8_t read_buffer[128];
while (1)
{
/* 从消息队列中接收消息 */
if (rt_mq_recv(&usb_mq, read_buffer, 34, RT_WAITING_FOREVER) > 0)
{
mytime_t* p_time_u16 = (mytime_t*)(read_buffer + 2);
monitor_info_u16_t* p_info_u16 = (monitor_info_u16_t *)(read_buffer + 2 + sizeof(mytime_t));
rt_kprintf("wYear %u\r\n", p_time_u16->wYear);
rt_kprintf("wMonth %u\r\n", p_time_u16->wMonth);
rt_kprintf("wDayOfWeek %u\r\n", p_time_u16->wDayOfWeek);
rt_kprintf("wDay %u\r\n", p_time_u16->wDay);
rt_kprintf("wHour %u\r\n", p_time_u16->wHour);
rt_kprintf("wMinute %u\r\n", p_time_u16->wMinute);
rt_kprintf("wSecond %u\r\n", p_time_u16->wSecond);
rt_kprintf("wMilliseconds %u\r\n", p_time_u16->wMilliseconds);
rt_kprintf("cpu_usage %u\r\n", p_info_u16->cpu_usage);
rt_kprintf("mem_usage %u\r\n", p_info_u16->mem_usage);
rt_kprintf("gpu_usage %u\r\n", p_info_u16->gpu_usage);
rt_kprintf("cpu_freq %u\r\n", p_info_u16->cpu_freq);
rt_kprintf("cpu_temperature %u\r\n", p_info_u16->cpu_temperature);
rt_kprintf("gpu_temperature %u\r\n", p_info_u16->gpu_temperature);
rt_kprintf("board_temperature %u\r\n", p_info_u16->board_temperature);
lv_label_set_text_fmt(guider_ui.screen_label_cpu_temp, "%2d", p_info_u16->cpu_temperature);
lv_label_set_text_fmt(guider_ui.screen_label_gpu_temp, "%2d", p_info_u16->gpu_temperature);
lv_label_set_text_fmt(guider_ui.screen_label_cpu_load, "%2d", p_info_u16->cpu_usage);
lv_label_set_text_fmt(guider_ui.screen_label_gpu_load, "%2d", p_info_u16->gpu_usage);
lv_arc_set_value(guider_ui.screen_arc_gpu_load, p_info_u16->gpu_usage);
lv_arc_set_value(guider_ui.screen_arc_gpu_temp, p_info_u16->gpu_temperature);
lv_label_set_text_fmt(guider_ui.screen_time, "%02d:%02d", p_time_u16->wHour, p_time_u16->wMinute);
lv_label_set_text_fmt(guider_ui.screen_date, "%02d.%02d.%02d", p_time_u16->wYear, p_time_u16->wMonth, p_time_u16->wDay);
}
}
成品效果
目前支持时间、日期、CPU/GPU 占用率和温度信息展示,其他信息还在完善中。

下位机代码可见 GitHub:
https://github.com/POMIN-163/FRDM-MCXN947-USB-Monitor
上位机代码:上位机代码.zip
如果你也对这类 RT-Thread 与 NXP 平台结合的开源实践感兴趣,可以从开源实战里继续挖掘更多源码分析、项目热度和贡献指南;遇到 USB 协议栈、LVGL 适配或 BSP 修改等具体问题,也建议对照技术文档中的架构设计与避坑指南查漏补缺。