在2024年全国大学生嵌入式芯片与系统设计竞赛中,各大高校学子纷纷展现出卓越的创新能力和扎实的技术功底。今天,特别为大家分享获奖作品——USB一线通监控副屏,它以其独特的设计和实用的功能赢得广泛好评与认可。
原文链接:https://club.rt-thread.org/ask/article/fd0a9bdab79b7c65.html
环境搭建
环境变量配置
为了提高一些编译的速度,选择了在Linux系统下进行开发,在Linux上开发N947需要先安装 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文件的这行代码:
代码高亮
这里使用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#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修改为RTT的API,例如如下这段代码:
并且 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的通讯,这里使用的是RTT官方推荐的CherryUSB这个开源USB协议栈:
将如下链接中的适配代码复制到工程中:
https://github.com/CherryUSB/cherryusb_mcx
因为传输的数据比较单一,这里使用串口屏的思路,直接用CDC_ACM的通讯方式,也就是在上位机显示为一个USB转串口设备,直接使用串口API完成通讯。
将RTT根目录中 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#include /* 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程序中端点收发的程序做如下修改,增加对于输入信息的回显:
oid usbd_cdc_acm_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes) { USB_LOG_RAW("actual out len:%d ", 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(" "); }
验证
然后插上开发板的USB HS那个USB接口,用串口工具发个数据包:
可以看到已经识别成了USB串行设备,PID 和VID也是我自己设置的0xE6E9和0x1122,后续上位机与开发板建立通讯锁定COM号就是依靠PID VID来查询实现,使用串口工具给开发板发送的数据也可以正常接收到。
上位机 时间原因上位机做的比较简单,实现了如下几个功能:
读取电脑的CPU、GPU的占用率和温度信息、获取当前时间
根据VID、PID查询COM来与开发板通讯,下发采集数据与时间
增加帧头后发送到下位机,固定长度32+2个字节
下位机数据更新 在开发板端增加一个thread来负责把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 ", 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(" "); 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 ", p_time_u16->wYear); rt_kprintf("wMonth %u ", p_time_u16->wMonth); rt_kprintf("wDayOfWeek %u ", p_time_u16->wDayOfWeek); rt_kprintf("wDay %u ", p_time_u16->wDay); rt_kprintf("wHour %u ", p_time_u16->wHour); rt_kprintf("wMinute %u ", p_time_u16->wMinute); rt_kprintf("wSecond %u ", p_time_u16->wSecond); rt_kprintf("wMilliseconds %u ", p_time_u16->wMilliseconds); rt_kprintf("cpu_usage %u ", p_info_u16->cpu_usage); rt_kprintf("mem_usage %u ", p_info_u16->mem_usage); rt_kprintf("gpu_usage %u ", p_info_u16->gpu_usage); rt_kprintf("cpu_freq %u ", p_info_u16->cpu_freq); rt_kprintf("cpu_temperature %u ", p_info_u16->cpu_temperature); rt_kprintf("gpu_temperature %u ", p_info_u16->gpu_temperature); rt_kprintf("board_temperature %u ", 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 的占用率和温度信息,其他的信息还在完善当中。
全部0条评论
快来发表一下你的评论吧 !