- 2024-03-20
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十六、build.sh源码分析
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
十一、SPI驱动LCD
十二、实现FrameBuffer设备及LVGL应用
十三、五向开关作为LVGL输入设备
十四、LVGL显示DHT11的温湿度
十五、MPU6050的内核驱动
出于对Luckfox SDK进行内核构建所用build.sh脚本的好奇(手里还有一个吃灰的RK3568板子,最近发现它的SDK也是利用了build.sh,猜想是瑞芯微原厂方案),近期花时间把它的源码整个分析了一下,记录于本篇测评报告。注:分析过程利用了文心一言结合自己的理解,若有错误欢迎大家指正。
1、开篇部分的变量定义和导出
build.sh直接运行的代码集中在文件开篇和结尾部分,中间大段篇幅则是bash函数的定义。而开篇部分代码主要是各种变量的定义和导出,比如下述是build.sh的前14行代码:
#!/bin/bash
set -eE
export LC_ALL=C
export LD_LIBRARY_PATH=
function unset_env_config_rk()
{
local tmp_file=`mktemp`
env | grep -oh "^RK_.*=" > $tmp_file || true
source $tmp_file
rm -f $tmp_file
}
unset_env_config_rk
##############################################################################
# 总结,上述代码主要工作:
# 1. "set -eE"设置脚本在遇到错误时退出:
# -e: 如果任何语句的执行结果不是true,则退出bash;
# -E: 当脚本成功完成时,其退出状态是0,非零值通常表示某种错误。
# 2. "export LC_ALL=C"设置了语言环境(C语言)以避免本地化差异。
# 3. "export LD_LIBRARY_PATH="清空了LD_LIBRARY_PATH环境变量。
# 4. 定义了一个函数"unset_env_config_rk",并调用:
# 该函数从当前环境变量中筛选出所有以RK_开头的变量;
# 将RK_开头变量存放在临时文件mktemp中,
# source临时文件,即设置其中的RK_开头变量成为环境变量它们;
# 删除临时文件(实际测试一般没有RK_开题变量)。
##############################################################################
接着,利用realpath和dirname命令获取build.sh文件自身所在的路径,进而得到Luckfox SDK的绝对路径了。本人也是在分析到这里时发现了SDK根目录上的build.sh实际是一个软链接,指向<SDK>/project/build.sh。
##############################################################################
# Global Variable Configure
##############################################################################
_FDS="\\ \n" # _DFS:“反斜杠+回车”即字符串转换拼贴
cmd=`realpath $0` # realpath:本文件($0表示)的绝对路径
COMMON_DIR=`dirname $cmd` # dirname:路径部分即不包含脚本文件名
# 实际:COMMON_DIR=<SDK>/project/
# 根目录build.sh是一个软链接,指向project/build.sh
PROJECT_TOP_DIR=$(realpath $COMMON_DIR/) # 指向<SDK>/project
SDK_ROOT_DIR=$(realpath $COMMON_DIR/..) # COMMON_DIR父目录即SDK根目录绝对路径
SDK_SYSDRV_DIR=${SDK_ROOT_DIR}/sysdrv # 指向<SDK>/sysdrv
SDK_MEDIA_DIR=${SDK_ROOT_DIR}/media # 指向<SDK>/media
SDK_APP_DIR=${PROJECT_TOP_DIR}/app # 指向<SDK>/project/app
BOARD_CONFIG=$SDK_ROOT_DIR/.BoardConfig.mk # BOARD_CONFIG指向.BoardConfig.mk
TARGET_PRODUCT_DIR=${PROJECT_TOP_DIR}/cfg # 指向<SDK>/project/cfg
GLOBAL_ROOT_FILESYSTEM_NAME=rootfs # 根文件系统名称
GLOBAL_OEM_NAME=oem # oem名称
GLOBAL_FS_TYPE_SUFFIX=_fs_type # 文件系统类型后缀
GLOBAL_INITRAMFS_BOOT_NAME="" # initramfs的启动名称,此处空
GLOBAL_PARTITIONS="" # 分区信息,此处空
GLOBAL_SDK_VERSION="" # SDK版本,此处空
######################################################
# if语句检查当前系统的CPU核心数:
# 如果只有一个核心在线(getconf _NPROCESSORS_ONLN返回1)
# 则RK_JOBS被设置为1
# 如果有多个核心在线,则RK_JOBS被设置为在线核心数减1。
# 这个变量通常用于make命令的-j选项,以并行地执行构建任务。
######################################################
if [ `getconf _NPROCESSORS_ONLN` -eq 1 ]; then
export RK_JOBS=1
else
export RK_JOBS=$((`getconf _NPROCESSORS_ONLN` - 1 ))
fi
export RK_BUILD_VERSION_TYPE=RELEASE # 构建版本类型RELEASE
######################################################
# SDK_ROOT_DIR:再次导出SDK的根目录路径。
# RK_PROJECT_OUTPUT:设置项目输出的根目录。
# RK_PROJECT_TOP_DIR:设置项目顶级目录。
# RK_PROJECT_PATH_MEDIA、RK_PROJECT_PATH_SYSDRV、
# RK_PROJECT_PATH_APP:设置项目不同部分的输出目录。
# RK_PROJECT_OUTPUT_IMAGE:设置输出镜像的目录。
# RK_PROJECT_PATH_RAMDISK、RK_PROJECT_PATH_FASTBOOT等:
# 设置其他特定输出目录。
# PATH:添加RK_PROJECT_PATH_PC_TOOLS到PATH中,
# 这样在执行命令时可以直接访问该目录下的工具。
# RK_PROJECT_FILE_ROOTFS_SCRIPT、
# RK_PROJECT_FILE_OEM_SCRIPT:设置不同脚本文件的路径。
# RK_PROJECT_TOOLS_MKFS_*:
# 设置创建不同文件系统所需工具的脚本路径。
######################################################
export SDK_ROOT_DIR=$SDK_ROOT_DIR
export RK_PROJECT_OUTPUT=$SDK_ROOT_DIR/output/out
export RK_PROJECT_TOP_DIR=$PROJECT_TOP_DIR
export RK_PROJECT_PATH_MEDIA=$SDK_ROOT_DIR/output/out/media_out
export RK_PROJECT_PATH_SYSDRV=$SDK_ROOT_DIR/output/out/sysdrv_out
export RK_PROJECT_PATH_APP=$SDK_ROOT_DIR/output/out/app_out
export RK_PROJECT_PATH_PC_TOOLS=$SDK_ROOT_DIR/output/out/sysdrv_out/pc
export RK_PROJECT_OUTPUT_IMAGE=$SDK_ROOT_DIR/output/image
export RK_PROJECT_PATH_RAMDISK=$SDK_ROOT_DIR/output/out/ramdisk
export RK_PROJECT_PATH_FASTBOOT=$SDK_ROOT_DIR/output/out/fastboot
export RK_PROJECT_PATH_RAMDISK_TINY_ROOTFS=$RK_PROJECT_PATH_RAMDISK/tiny_rootfs
export PATH=$RK_PROJECT_PATH_PC_TOOLS:$PATH
export RK_PROJECT_FILE_ROOTFS_SCRIPT=$RK_PROJECT_OUTPUT/S20linkmount
export RK_PROJECT_FILE_OEM_SCRIPT=$RK_PROJECT_OUTPUT/S21appinit
export RK_PROJECT_FILE_RECOVERY_SCRIPT=$RK_PROJECT_PATH_RAMDISK_TINY_ROOTFS/etc/init.d/S10linkdev
export RK_PROJECT_FILE_RECOVERY_LUNCH_SCRIPT=$RK_PROJECT_PATH_RAMDISK_TINY_ROOTFS/etc/init.d/S99lunch_recovery
export RK_PROJECT_TOOLS_MKFS_SQUASHFS=mkfs_squashfs.sh
export RK_PROJECT_TOOLS_MKFS_EXT4=mkfs_ext4.sh
export RK_PROJECT_TOOLS_MKFS_UBIFS=mkfs_ubi.sh
export RK_PROJECT_TOOLS_MKFS_JFFS2=mkfs_jffs2.sh
export RK_PROJECT_TOOLS_MKFS_ROMFS=mkfs_romfs.sh
export RK_PROJECT_TOOLS_MKFS_EROFS=mkfs_erofs.sh
export RK_PROJECT_TOOLS_MKFS_INITRAMFS=mkfs_initramfs.sh
######################################################
# RK_PROJECT_ROOTFS_TYPE:可能根文件系统的类型。
# OTA_SCRIPT_PATH:设置OTA(Over-The-Air)脚本的路径。
# ENV_CFG_FILE:设置环境配置文件的路径。
# ENV_SIZE 和 ENV_OFFSET:可能存储环境变量的大小和偏移量。
######################################################
RK_PROJECT_ROOTFS_TYPE=""
OTA_SCRIPT_PATH=$RK_PROJECT_PATH_RAMDISK
ENV_CFG_FILE=$RK_PROJECT_OUTPUT_IMAGE/.env.txt
ENV_SIZE=""
ENV_OFFSET=""
2、结尾部分的实际业务逻辑代码
build.sh结尾部分是实际的业务逻辑,也就是执行:build.sh + 参数项的执行代码:
#=========================
# build targets
#=========================
# 设置了当脚本中任何命令返回非零退出状态时执行的错误处理函数err_handler。
trap 'err_handler' ERR
# 切换到脚本定义的$PROJECT_TOP_DIR目录,即<SDK>/project
cd $PROJECT_TOP_DIR
# 调用函数:从当前 shell 环境中移除所有与 RK_ 前缀相关的环境变量,
# 这些环境变量通常是在 BoardConfig*.mk 文件中通过 export 语句定义的。
unset_board_config_all
# ./build.sh lunch即sh脚本执行时第一个参数为lunch,则执行build_select_board LUNCH-FORCE
if [ "$1" = "lunch" ];then
# 选择并设置目标开发板的构建配置(见260行),即选择开发板更新defconfig文件
build_select_board LUNCH-FORCE
fi
# 如果$BOARD_CONFIG指定的配置文件不存在,则调用build_select_board函数。
# 即从未执行./build.sh lunch,那么需要强制进入设置目标开发板的阶段
if [ ! -e "$BOARD_CONFIG" ];then
build_select_board
fi
# 如果$BOARD_CONFIG是一个符号链接,则执行source命令来加载它,用于导入环境变量或函数定义。
# BOARD_CONFIG指向.BoardConfig.mk,即./build.sh lunch得到的开发板配置文件
[ -L "$BOARD_CONFIG" ] && source $BOARD_CONFIG
# 设置交叉编译工具链和将其路径添加到PATH环境变量中。
# (之前一直好奇Luckfox编译镜像不要设置环境变量,因为这里做了设置)
export RK_PROJECT_TOOLCHAIN_CROSS=$RK_TOOLCHAIN_CROSS
export PATH="${SDK_ROOT_DIR}/tools/linux/toolchain/${RK_PROJECT_TOOLCHAIN_CROSS}/bin":$PATH
# 如果目标根文件系统是ubuntu,则根据LF_SUBMODULES_BY的值来复制对应的.gitmodules文件,并更新Git子模块。
if [[ "$LF_TARGET_ROOTFS" = "ubuntu" ]];then
if [[ "$LF_SUBMODULES_BY" = "github" ]];then
cp ${SDK_ROOT_DIR}/.gitmodules.github ${SDK_ROOT_DIR}/.gitmodules
else
if [[ "$LF_SUBMODULES_BY" = "gitee" ]];then
cp ${SDK_ROOT_DIR}/.gitmodules.gitee ${SDK_ROOT_DIR}/.gitmodules
else
exit 0
fi
fi
git submodule update --init --recursive
fi
# 如果命令行参数中包含help或-h,则显示帮助信息。
if echo $@|grep -wqE "help|-h"; then
# 应该是分析是否有第2参数,若有则进一步调用usage$2来输出子参数信息。
# 比如:./build.sh help exec,那么就会输出exec的帮助信息。
# 后经实际测试,二级信息输出只支持:media、sysdrv、kernel、uboot、rootfs
# 即有函数usagemedia等。
if [ -n "$2" -a "$(type -t usage$2)" == function ]; then
echo "###Current Configure [ $2 ] Build Command###"
eval usage$2
else
usage
fi
exit 0
fi
# 检查指定的交叉编译gcc是否存在,如果不存在则输出错误信息并退出脚本。
if ! ${RK_PROJECT_TOOLCHAIN_CROSS}-gcc --version &> /dev/null; then
msg_error "Not found toolchain ${RK_PROJECT_TOOLCHAIN_CROSS}-gcc for [$RK_CHIP] !!!"
msg_info "Please run these commands to install ${RK_PROJECT_TOOLCHAIN_CROSS}-gcc"
echo ""
echo " cd ${SDK_ROOT_DIR}/tools/linux/toolchain/${RK_PROJECT_TOOLCHAIN_CROSS}/"
echo " source env_install_toolchain.sh"
echo ""
exit 1
fi
# 根据RK_PROJECT_TOOLCHAIN_CROSS的值设置库类型(uclibc或glibc)。
# 看来Luckfox基于RV1106应该是导入glibc库了。
case $RK_PROJECT_TOOLCHAIN_CROSS in
arm-rockchip830-linux-uclibcgnueabihf)
export RK_LIBC_TPYE=uclibc
;;
*)
export RK_LIBC_TPYE=glibc
;;
esac
# 设置构建过程中使用的各种输出目录路径。
export RK_PROJECT_PACKAGE_ROOTFS_DIR=$RK_PROJECT_OUTPUT/rootfs_${RK_LIBC_TPYE}_${RK_CHIP}
export RK_PROJECT_PACKAGE_OEM_DIR=$RK_PROJECT_OUTPUT/oem
export RK_PROJECT_PACKAGE_USERDATA_DIR=$RK_PROJECT_OUTPUT/userdata
export RK_PROJECT_PATH_BOARD_BIN=$RK_PROJECT_PATH_SYSDRV/board_${RK_LIBC_TPYE}_${RK_CHIP}
# 调用build_check和__PREPARE_BOARD_CFG函数进行构建前的检查和准备板级配置。
build_check
__PREPARE_BOARD_CFG
# $#传递当前脚本(即./build.sh)运行的输入参数个数,这个数值存在num中。
num=$#
option=""
# 最终的build.sh执行逻辑
# 脚本命令包含参数1,则根据参数执行不同的功能(前面定义的函数)。
# 不同的功能函数实际先存储在变量option中。
# 其中clean需要参数2,以确定clean的部分。
while [ $# -ne 0 ]
do
case $1 in
DEBUG) export RK_BUILD_VERSION_TYPE=DEBUG;;
all) option=build_all ;;
save) option=build_save ;;
allsave) option=build_allsave ;;
check) option=build_check ;;
clean) option="build_clean $2";break;;
firmware) option=build_firmware ;;
ota) option=build_ota ;;
updateimg) option=build_updateimg ;;
unpackimg) option=build_unpack_updateimg ;;
factory) option=build_factory ;;
recovery) option=build_recovery ;;
env) option=build_env ;;
meta) option=build_meta ;;
driver) option=build_driver ;;
sysdrv) option=build_sysdrv ;;
uboot) option=build_uboot ;;
kernel) option=build_kernel ;;
rootfs) option=build_rootfs ;;
media) option=build_media ;;
app) option=build_app ;;
info) option=build_info ;;
tool) option=build_tool ;;
*) option=usage ;;
esac
if [ $((num)) -gt 0 ]; then
shift
fi
done
# 最后利用eval命令执行option指向的函数功能
eval "${option:-build_allsave}"
上述代码先判断是否执行了“build.sh lunch”命令,进而实现开发板选型的逻辑。最后的while循环决定了build.sh可以附带的其它命令参数,比如本人实际使用过的:“build.sh clean”、“build.sh kernel”等。而这些命令会通过调用函数来实现,比如build_all()、build_clean()等,这些函数的定义代码也就是在文件的中间部分,限于篇幅这里不做介绍了,附上本人添加了注释后的build.sh文件(由于时间关系仅注释了一半的函数定义)。
- 2024-03-16
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十四、LVGL显示DHT11的温湿度
lugl4313820 发表于 2024-3-14 10:14
看了一下LVGL与stm32的TouchGFX来比,就显示差了很多了。
嗯,LVGL在Linux环境用也是有点勉强,不如QT。在MCU端应该是资源要求上要比TouchGFX低,适合中低端MCU吧。
- 2024-03-11
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】九、Python控制I2C驱动OLED
tt2nn 发表于 2024-3-11 15:16
请问基于ubuntu的固件应该如何驱动oled?
我没有做ubuntu的OLED驱动,不过大概流程应该差不多,比如:要先看看ubuntu下有没有I2C的设备文件,然后安装Python,smbus库。因为底层接口都是基于设备文件,所以只有I2C设备存在,Python应该很容易做好移植的。
如果不存在I2C设备文件,应该从设备树文件入手,重新编译内核。
- 2024-03-07
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十五、MPU6050的内核驱动
lugl4313820 发表于 2024-3-7 07:12
看了大佬出产了这么多的作品,肯定用心参与评测了。
谢谢赞赏,个人就觉得既然做了,就认真点吧。
- 2024-03-02
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十五、MPU6050的内核驱动
本帖最后由 sonicfirr 于 2024-3-2 21:12 编辑
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
十一、SPI驱动LCD
十二、实现FrameBuffer设备及LVGL应用
十三、五向开关作为LVGL输入设备
十四、LVGL显示DHT11的温湿度
本篇测评主要参考一篇博文:i2c子系统-----mpu6050(https://blog.csdn.net/qq_69255338/article/details/124005637),同时参考了Luckfox Wiki之温湿度传感器DHT11篇中的Makefile写法。
1、MPU6050的设备树节点
应该说是本次Luckfox幸狐RV1106板的测评活动促使自己开始真正了解Linux内核,前面测评I2C时也开始关注Linux源码的Document目录,所以在实验MPU6050时,首先想到从文档中搜索看看是否包含相关驱动。
图15-1 搜索mpu6050相关文档
在<Luckfox SDK>/sysdrv/source/kernel/Documentation目录下,执行命令:find -name *mpu6050*,找到了MPU6050的说明文档:./devicetree/bindings/iio/imu/inv_mpu6050.txt。
说明文档中给出了MPU6050在设备树文件中的节点描述的示例代码,如下截图所示,代码定义属性所代表的意义如下:
compatible = "invensense,mpu6050";:这个属性表示这个节点描述的硬件与"invensense,mpu6050"兼容。这通常用于驱动程序匹配,确保正确的驱动程序加载来支持这个硬件。
reg = <0x68>;:这个属性定义了MPU6050传感器的基地址,通常是它在I2C或SPI总线上的地址。
interrupt-parent = <&gpio1>;:这个属性指定了中断控制器的父节点,即MPU6050传感器将使用的GPIO(通用输入/输出)控制器。
interrupts = <18 IRQ_TYPE_EDGE_RISING>;:这个属性定义了MPU6050传感器使用的中断号(在这里是18)和中断类型(这里是边缘上升类型)。
mount-matrix:这是一个3x3的矩阵,用于描述MPU6050传感器在系统中的物理方向或安装方向。这个矩阵可以用于将传感器测量的方向转换到系统或设备的本地坐标系。
第一行和第二行定义了x轴和y轴的方向转换。
第三行定义了z轴的方向转换。
图15-2 mpu6050说明文档示例代码截图
本次实验只做MPU6050在Luckfox Pro Max下的驱动验证,所以不考虑接入传感器的IRQ信号(这个是MPU6050的中断输出信号,实际中可以在MPU6050中设置阈值,如果监测数据超限则产生唤醒电平,用以唤醒休眠中的处理器),也不考虑坐标转换了。所以最终的设备树文件rv1106g-luckfox-pico-pro-max.dts,在&i2c3节点内添加mpu6050节点:
&i2c3 {
status = "okay";
pinctrl-0 = <&i2c3m1_xfer>;
clock-frequency = <100000>;
// 下述为本例添加部分
mpu6050@68 { /*i2c client信息*/
compatible = "invensense,mpu6050";
reg = <0x68>;
};
};
2、MPU6050的驱动和测试程序
修改设备树后,依然是重新构建系统内核,并重新烧录开发板。当然,还需要编写MPU6050的驱动。
这里在虚拟机上创建一个目录,其中包含文件如下:
mpu6050.h —— 相关头文件,包含自定义联合体类型用以存放传感器读数;
mpu6050_drv.c —— 内核驱动源文件;
mpu6050_test.c —— 驱动测试程序源文件;
Makefile —— 针对两个源文件的编译命令等。
除了Makefile之外,其它文件的源码都是照搬参考文档1“i2c子系统-----mpu6050”,这里也就不再贴出来了,只是Makefile针对Luckfox SDK的情况做了改动,具体如下:
ARCH=arm
CROSS_COMPILE=~/luckfox-pico/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-
export ARCH CROSS_COMPILE
CC=$(CROSS_COMPILE)gcc
KERN_DIR = ~/luckfox-pico/sysdrv/source/kernel
PWD ?= $(shell pwd)
name=mpu6050
all:
make -C $(KERN_DIR) M=$(PWD) modules
$(CROSS_COMPILE)gcc $(name)_test.c -o $(name)
echo $(PWD)
clean:
rm -f *.ko *.o *.mod *.mod.o *.mod.c *.symvers *.order *.cmd
obj-m += $(name)_drv.o
make后,项目目录中包含内核驱动模块mpu6050_drv.ko和测试程序mpu6050,将这两个文件拷贝到本地,再adb push到开发板。
图15-3 mpu6050驱动项目目录
接着,进入开发板控制台,首先是安装内核模块,然后chmod赋予测试程序执行权限。最后,运行测试程序查看执行结果。
图15-4 mpu6050驱动模块安装(开发板上)
图15-5 mpu6050测试程序
- 2024-03-01
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】九、Python控制I2C驱动OLED
chejm 发表于 2024-2-28 21:25
非常感谢楼主分享的技术信息,内容详实,很有参考价值,值得收藏学习
谢谢支持
- 2024-02-24
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十四、LVGL显示DHT11的温湿度
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
十一、SPI驱动LCD
十二、实现FrameBuffer设备及LVGL应用
十三、五向开关作为LVGL输入设备
本篇记录Luckfox Pro Max开发板编写DHT11内核驱动,并将测出的温湿度通过LVGL框架显示到0.96寸LCD上的过程,并利用lv_font_conv工具自建字体文件实现图标显示。
1、DHT11内核驱动
DHT11的驱动主要参考Luckfox Wiki的“温湿度传感器DHT11模块使用”篇(https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-dht11)。文档上提供了DHT11.zip(百度网盘),其中包含内核驱动源文件dht11_drv.c、测试应用源文件dht11_test.c和Makefile,以及设备树文件。
不过官方提供的设备树文件使用GPIO1_C7,而这个管脚已经被用作LCD模块控制信号了。所以本人这里改用管脚GPIO2_PA1即板上25号脚,相应的设备树文件源码如下:
/ {
model = "Luckfox Pico Max";
compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1106";
/*DHT11*/
dht11_sensor {
compatible = "dht11";
pinctrl-names = "default";
status = "okay";
pinctrl-0 = <&gpio2_pa1>;
dht11[url=home.php?mod=space&uid=490]@1[/url] {
gpios = <&gpio2 RK_PA1 GPIO_ACTIVE_HIGH>;
label = "dht11";
linux,default-trigger = "humidity";
};
};
// 省略其它部分
}
&pinctrl {
/*DHT11*/
gpio2-pa1 {
gpio2_pa1:gpio2-pa1 {
rockchip,pins = <2 RK_PA1 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
// 省略其它部分
}
修改设备树文件rv1106g-luckfox-pico-pro-max.dts后,依然是编译内核并重新烧录系统。新系统启动后,开发板中会增加设备文件“/dev/dht11”,而案例提供的内核驱动和测试应用都是基于“/dev/dht11”文件的读写操作,所以不用修改代码,直接make即可生成测试程序。
图14-1 DHT11项目make后的生成文件
项目make后生成内核驱动模块文件dht11_drv.ko和应用程序文件dht11。这两个文件都要推送到开发板,然后利用命令“insmod dht11_drv.ko”安装内核模块,再赋予dht11执行权限,并连接传感器执行程序进行测试,开发板可以正确读取温湿度。
图14-2 dht11测试效果——每秒输出温湿度值
图14-3 LVGL显示温湿度并带温度计、湿度计图标
上图是将DHT11读取代码整合到lvgl_demo项目中的效果,这里显示了两个图标,另外温湿度值也是自建字体库实现的,main.c具体代码如下:
#include "lvgl/lvgl.h"
#include "DEV_Config.h"
#include "lv_drivers/display/fbdev.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
typedef unsigned char u8;
typedef unsigned short u16;
// 存放DHT11读数的结构体类型
typedef struct DHT11_SENSOR_DATA
{
u16 temp; // Temperature
u16 hum; // Humidity
} dht11_data;
// “Ctrl+C”触发sigint_handler()并退出程序
volatile sig_atomic_t exit_flag = 0;
void sigint_handler(int signo)
{
if (signo == SIGINT)
{
exit_flag = 1;
}
}
/**
申明字体:myfont.c自建方正姚体、iconfont30.c是两个图标
*/
LV_FONT_DECLARE(myfont);
LV_FONT_DECLARE(iconfont30);
// 两个图标字串(图标的utf-8编码)采用宏定义形式
#define ICON_TEMP "\xEE\x9A\x92"
#define ICON_HUMI "\xEE\x99\xA9"
/**
by Firr. 五向按键做LVGL输入设备——本例输入功能未使用
*/
// 按键读取函数——返回按键对应IO编号以区分不同的按键
uint8_t getKeyValue(void) {
if(GET_KEY_RIGHT == 0) return KEY_RIGHT_PIN;
else if(GET_KEY_LEFT == 0) return KEY_LEFT_PIN;
else if(GET_KEY_UP == 0) return KEY_UP_PIN;
else if(GET_KEY_DOWN == 0) return KEY_DOWN_PIN;
else if(GET_KEY_PRESS == 0)return KEY_PRESS_PIN;
else return 0;
}
// 自定义输入事件接口——输入读取回调
void keypadRead(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
static uint32_t last_key = 0;
// 读取按键值——即按键对应IO编号,作为“输入事件的数值”
uint32_t act_key = getKeyValue();
if(act_key != 0) {
// 有按键按下,则修改“输入事件的状态”
data->state = LV_INDEV_STATE_PR;
/* 转换按键值为“LVGL控件字符(LVGL control characters)” */
switch(act_key) {
case KEY_LEFT_PIN:
act_key = LV_KEY_LEFT; // 减少值或向左移动
break;
case KEY_RIGHT_PIN:
act_key = LV_KEY_RIGHT; // 减少值或向右移动
break;
case KEY_UP_PIN:
act_key = LV_KEY_PREV; // 聚焦到上一对象
break;
case KEY_DOWN_PIN:
act_key = LV_KEY_NEXT; // 聚焦到下一对象
break;
case KEY_PRESS_PIN:
act_key = LV_KEY_ENTER; // 触发LV_EVENT_PRESSED/CLICKED/LONG_PRESSED
break;
}
last_key = act_key;
} else {
data->state = LV_INDEV_STATE_REL;
}
// 将不同按键转化为LVGL定义的“输入事件key”
data->key = last_key;
}
// LVGL显示缓存区及显示回调声明
#define DISP_BUF_SIZE (160 * 128)
void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);
// 按键设备指针——用于设置给“按键设备”设置“(焦点)组”
lv_indev_t *indev_keypad;
// 温湿度界面显示函数
void fontExample(dht11_data* data) {
char result[100] = {0};
lv_obj_clean(lv_scr_act()); // 清屏
// 创建风格,用以设置字体
static lv_style_t icon_style; // 图标字体
lv_style_init(&icon_style);
lv_style_set_text_font(&icon_style, &iconfont30);
static lv_style_t text_style; // 文字字体
lv_style_init(&text_style);
lv_style_set_text_font(&text_style, &myfont);
// 显示温度计图标
lv_obj_t *temp_icon = lv_label_create( lv_scr_act() );
lv_label_set_text(temp_icon, ICON_TEMP);
lv_obj_align(temp_icon, LV_ALIGN_LEFT_MID, 25, -10);
lv_obj_add_style(temp_icon, &icon_style, LV_PART_MAIN);
// 显示温度值
lv_obj_t *temp_text = lv_label_create( lv_scr_act() );
sprintf(result, "%02d C", data->temp >> 8);
lv_label_set_text(temp_text, result);
lv_obj_align(temp_text, LV_ALIGN_LEFT_MID, 10, 20);
lv_obj_add_style(temp_text, &text_style, LV_PART_MAIN);
// 显示湿度计图标
lv_obj_t *humi_icon = lv_label_create( lv_scr_act() );
lv_label_set_text(humi_icon, ICON_HUMI);
lv_obj_align(humi_icon, LV_ALIGN_RIGHT_MID, -25, -10);
lv_obj_add_style(humi_icon, &icon_style, LV_PART_MAIN);
// 显示湿度值
lv_obj_t *humi_text = lv_label_create( lv_scr_act() );
sprintf(result, "%02d %%", data->hum >> 8);
lv_label_set_text(humi_text, result);
lv_obj_align(humi_text, LV_ALIGN_RIGHT_MID, -10, 20);
lv_obj_add_style(humi_text, &text_style, LV_PART_MAIN);
}
int main(void)
{
int fd; // DHT11文件描述符
int retval;
int counter = 0; // 计数变量
dht11_data Curdht11_data; // DHT11读数
signal(SIGINT, sigint_handler);
printf("Press CTRL+C to exit.\n");
/*打开DHT11设备文件*/
fd = open("/dev/dht11", O_RDONLY);
if (fd == -1)
{
perror("open dht11 error\n");
exit(-1);
}
sleep(1);
printf("open /dev/dht11 successfully\n");
/*LittlevGL init*/
lv_init();
/*Linux frame buffer device init*/
fbdev_init();
/*A small buffer for LittlevGL to draw the screen's content*/
static lv_color_t buf[DISP_BUF_SIZE];
/*Initialize a descriptor for the buffer*/
static lv_disp_draw_buf_t disp_buf;
lv_disp_draw_buf_init(&disp_buf, buf, NULL, DISP_BUF_SIZE);
/*Initialize and register a display driver*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &disp_buf;
disp_drv.flush_cb = fbdev_flush;
disp_drv.hor_res = 160;
disp_drv.ver_res = 128;
lv_disp_drv_register(&disp_drv);
/*Initialize pin*/
DEV_ModuleInit();
/* lvgl输入初始化 */
static lv_indev_drv_t indev_drv; // 输入设备实例初始化
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_KEYPAD;
indev_drv.read_cb = keypadRead; // 输入事件回调
indev_keypad = lv_indev_drv_register(&indev_drv); // 注册输入设备获取返回指针
// 显示应用UI——先做一次显示因为DHT11未检测,所以初运行时显示0值
fontExample(&Curdht11_data);
/*Handle LitlevGL tasks (tickless mode)*/
while(!exit_flag) {
lv_timer_handler();
usleep(5000);
if(counter++ >= 1000) {
counter = 0;
retval = read(fd, &Curdht11_data, sizeof(Curdht11_data));
if (retval != -1)
{
fontExample(&Curdht11_data);
}
if (Curdht11_data.temp != 0xffff)
printf("Temperature:%d.%d C, Humidity:%d.%d %%RH\n", Curdht11_data.temp >> 8, Curdht11_data.temp & 0xff, \
Curdht11_data.hum >> 8, Curdht11_data.hum & 0xff);
}
}
close(fd);
sleep(1);
return 0;
}
/*Set in lv_conf.h as `LV_TICK_CUSTOM_SYS_TIME_EXPR`*/
uint32_t custom_tick_get(void)
{
static uint64_t start_ms = 0;
if(start_ms == 0) {
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
}
struct timeval tv_now;
gettimeofday(&tv_now, NULL);
uint64_t now_ms;
now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;
uint32_t time_ms = now_ms - start_ms;
return time_ms;
}
2、LVGL字体自建
上述案例使用的字体myfont和iconfont30,实际是myfont.c和iconfont30.c两个源文件,与main.c同在项目根目录,由LVGL提供的字体工具lv_font_conv生成。
LVGL提供在线方式和离线方式两种自定义字体的方法,这里使用离线方式(需要Node.js)。
图14-4 LVGL自定义字体文档截图
lv_font_conv是LVGL官方编写的一套离线字体转换工具,由于此工具是由node.js编写的,所以要在第一步安装node运行环境。
离线工具没有图形化界面,需要使用命令行输入命令来转换,要比在线复杂点,优点就是无需网络,而且速度非常快。下述是其项目链接及使用流程图,gitee上有一些个人用户fork的版本,访问速度更快些。
lv_font_conv GitHub链接:https://github.com/lvgl/lv_font_conv 。
lv_font_conv gitee个人fork版:https://gitee.com/dmcus/lv_font_conv
图14-5 lv_font_conv使用流程图
图14-6 lv_font_conv全局安装(npm i lv_font_conv -g)
lv_font_conv建议通过npm进行全局安装(命令如上图所示)。然后,就是选取字体文件(*.ttf格式),这里选取系统自带字体“方正姚体”作为示例——当然也可以网上下载其它有特色的字体文件。
系统字体位于“..\Windows\Fonts”目录下,在其中找到“方正姚体 常规(实际文件名FZYTK.TTF)”,然后拷贝到自建文件夹下以方便后续操作。
图14-7 拷贝字体文件
接着,命令行进入到自建文件夹目录,通过lv_font_conv进行字体转换,比如:
lv_font_conv --no-compress --format lvgl --font ./FZYTK.TTF -o ./myfont.c --bpp 4 --size 25 --symbols 天津城建大学 -r 0x20-0x7F
图14-8 命令行转换字体
上述命令生成“方正姚体 25号 英文数字+天津城建大学六个汉字”的字体库源文件“myfont.c”。命令相关参数项解释如下:
--no-compress: 不压缩
--format lvgl: 输出格式LVGL(还支持bin格式,不过需要引入文件系统)
--font ./FZYTK.TTF 要转换的字体文件(当前目录下的FZYTK.TTF)
-o ./myfont.c 输出文件的路径及文件名
--bpp 4 抗锯齿大小设置为4
--size 25 输出字体为25像素高
--symbols 天津城建大学 要转换的字符“天津城建大学”
-r 0x20-0x7F ASCII编码范围(即全部可见ASCII字符)
另外,还要注意一点,lv_font_conv默认创建的是UTF-8编码字符,针对使用的IDE,需要注意设置编码为“UTF-8”,否则字体加载会失效。
3、图标以字体方式导入
其实LVGL中已经集成了很多图标字体,如:WIFI,蓝牙,保存,复制,等图标。那如果项目中需要用到其它没有的图标,就要用到阿里的一个免费的图标字体平台“iconfont”。iconfont平台允许用户自定义下载多种格式的icon,平台也可将图标转换为字体文件,用户可以免费注册使用。
使用图标字体的关键点有两个:一是得到图标字体的ttf文件;二是图标字体编码转UTF-8——iconfont平台提供的是Unicode统一码。
我们首先来获取图标字体的ttf文件,这需要申请iconfont账户并登录,然后建立“项目”,并将选取的图标加入购物车再导入项目,最后下载项目。项目以压缩包形式下载,其中就包含ttf文件。
图14-9 iconfont得到图标字体文件
上图可以看到,我们下载的图标文件包含两个图标,其Unicode编码对应16进制分别为:湿度计图标--0xe669,温度计图标--0xe692(图中“&#x<16进制数>;”是HTML实体编码形式,其16进制数就是对应Unicode码)。
导出的“iconfont.ttf”文件用同样的方法转换为lvgl字体库,命令示例:
lv_font_conv --no-compress --format lvgl --font ./iconfont.ttf -o ./iconfont30.c --bpp 4 --size 30 -r 0xe669,0xe692
上述命令生成字码库源文件“iconfont30.c”,图标大小30像素,其中重点关注输入参数“-r”,用于设置所需字符的unicode编码范围,这里表示设置“0xe669和0xe692”两个字符。
图14-10 lv_font_conv命令的-r参数传值示例
可见lv_font_conv在转码时需要用到Unicode编码,但是源程序中则需要使用UTF-8编码(实际UTF-8是Unicode的一种存储方式上的编码)。
UTF-8的编码规则很简单,只有两条:
对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。
图14-11 UTF-8编码规则示意图
依照上述规则,本人编写了一段JS脚本用以进行编码转换,利用Node执行脚本,直接在命令行后将两个字符的Unicode码作为输入参数,执行后控制台输出转换的UTF-8编码——且以C语言字符串形式给出。
/**
* Unicode --> UTF-8,传参和返回都是Number型
* @param {Unicode编码数值} unicode
* @returns UTF-8编码数组
*/
const unicodeToUTF8 = (unicode) => {
if(unicode>=0x00000000 && unicode<=0x0000007F) {
return unicode;
} else if(unicode>=0x00000080 && unicode<=0x000007FF) {
var r1 = (((unicode & 0x7C0) >> 6) | 0xC0) << 8;
var r2 = (unicode & 0x03F) | 0x80;
return r1 | r2;
} else if(unicode>=0x00000800 && unicode<=0x0000FFFF) {
var r1 = (((unicode & 0xF000) >> 12) | 0xE0) << 16;
var r2 = (((unicode & 0x0FC0) >> 6) | 0x80) << 8;
var r3 = ((unicode & 0x003F) | 0x80);
return r1 | r2 | r3;
} else if(unicode>=0x00010000 && unicode<=0x0010FFFF) {
var r1 = (((unicode & 0x1C0000) >> 18) | 0xE0) << 24;
var r2 = (((unicode & 0x03F000) >> 12) | 0x80) << 16;
var r3 = (((unicode & 0x000FC0) >> 6) | 0x80) << 8;
var r4 = ((unicode & 0x00003F) | 0x80);
return r1 | r2 | r3 | r4;
} else {
return false;
}
}
/**
* 将Number型传参转为字节数组
* @param {Number型数值} num
* @returns 对应字节数组
*/
const numberToBytes = (num) => {
let bytes = [];
while(num > 0) {
let byte = num & 0xFF; // 取得最低8位
bytes.unshift(byte); // 将这个字节放到数组的开始
num >>= 8; // 右移8位,相当于除以256
}
return bytes;
}
/**
* process.argv是Node执行JS脚本时的输入命令行构成的数组
* 索引0为Node命令,索引1为js文件名
* 从索引2开始是命令行输入参数
* 比如:当前脚本执行命令“node .\utf.js 0xe669 0xe692”
* 控制台输出0xe669 0xe692的对应转换结果:"\xEE\x99\xA9" "\xEE\x9A\x92"
*/
let result = '';
// console.log(process.argv);
for(let i=2; i<process.argv.length; i++) {
let num = parseInt(process.argv[i], 16); // 输入参数即16进制字串转整数
let resNum = unicodeToUTF8(num);
let resBytes = numberToBytes(resNum);
let resStr = resBytes.map(b => `\\x${b.toString(16).toUpperCase()}`).join('');
result += `"${resStr}" `;
}
// 输出最终转换结果
console.log(result);
图14-12 UTF-8转换脚本执行展示
上图可以看到两个图标字体最终的UTF-8编码为:湿度计图标--"\xEE\x99\xA9" 温度计图标--"\xEE\x9A\x92"。这两个输出就是lvgl_demo案例中关于温度计宏定义值的由来。
- 2024-02-20
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十三、五向开关作为LVGL输入设备
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
十一、SPI驱动LCD
十二、实现FrameBuffer设备及LVGL应用
由于本人目前使用合宙0.96寸LCD作为Luckfox Pro Max测评的显示设备,且LCD模块板载五向开关,于是尝试利用五向开关作为LVGL的输入设备。
1、LVGL内部节拍原理
上一篇测评中实际使用了Luckfox官方案例的输入实现方案,自主判断按键并产生不同的逻辑功能。LVGL作为一款流行的GUI库,控件提供了丰富的事件回调接口,如果不能为项目注册输入设备,其控件的事件机制也就无法使用了。
对于LVGL框架本身,需要依靠lv_timer_handler()产生其系统节拍——本质上就是LVGL运行后的一个“大轮训”。目前的案例中,程序启动后大概每5ms调用一次lv_timer_handler()函数,而此函数就是对定时器链表_lv_timer_ll的遍历。
图13-1 lv_timer_handler()核心代码
上图这段代码包含了两层循环。
首先,外层循环通过while(LV_GC_ROOT(_lv_timer_act))条件来控制循环的次数。这个循环的作用是从定时器链表中获取一个元素,并执行定时器的执行函数lv_timer_exec()。
在内层循环中,代码首先通过_lv_ll_get_next()函数获取下一个元素,以便在处理当前定时器后能够加载下一个定时器。然后,它检查lv_timer_exec()函数的返回值来确定是否执行了定时器的创建或删除操作。如果执行了这些操作,那么当前定时器或下一个定时器可能已经被破坏,所以需要重新从第一个定时器开始处理。这就需要通过跳出内层循环来实现了。
因此,两层循环的逻辑是:外层循环从定时器链表中获取一个元素并执行,内层循环在处理完当前定时器后检查是否需要重新开始处理。这样设计可以确保所有的定时器都会被处理,并且在有定时器被创建或删除时能够正确地重新开始处理。
经过分析,lv_timer_handler()就是一个软件定时器链表的处理器,在项目中间隔5ms执行一次,也就意味着每5ms维护一次LVGL线程。
具体LVGL线程的维护工作中最重要的两点:一是UI刷新(更新和绘制UI界面的过程,以响应用户的操作和数据变化),二是输入事件捕获(按下按键或点击触屏,GUI框架会自动调用相应的事件处理回调,并将事件对象传递给该函数)。
LVGL的UI刷新,也就是显示功能,在案例代码中已经知道利用lv_disp_drv_t结构体变量实现——包含:屏幕尺寸、UI缓存、刷新回调。而注册这个结构体变量的lv_disp_drv_register()函数中就创建了定时器(默认周期50ms)。同样,输入设备注册函数lv_indev_drv_register()也创建了定时器(默认周期50ms)。
所以说,用户代码实现每5ms一次的定时器链表处理,默认情况下,每10次即50ms,UI刷新和输入捕获的定时器都会溢出并执行回调,进而完成LVGL基本维护工作。这就是LVGL内部节拍的原理。
图13-2 lv_indev_drv_register核心代码(宏LV_INDEV_DEF_READ_PERIOD默认10即10个节拍)
图13-3 输入设备定时器回调lv_indev_read_timer_cb()核心代码
图13-4 _lv_indev_read()代码实现
由上述分析可得到,lv_indev_drv_register()注册输入驱动——indev_drv即出入参数(类型:lv_indev_drv_t),而这个驱动会进一步封装到lv_indev_t * indev(姑且称为输入设备描述符)中,而且创建的输入定时器也会封装在indev_drv中(见indev->driver->read_timer部分)。
接着,输入定时器的回调lv_indev_read_timer_cb()会调用_lv_indev_read()由注册输入设备描述符得到输入事件,再根据注册输入设备的类型不同调用不同的函数实现用以处理输入事件。本例使用五向开关,输入设备注册为类型LV_INDEV_TYPE_KEYPAD,也就是最后依靠函数indev_keypad_proc来处理输入事件——而具体的五向开关输入事件则是依靠自定义的输入设备驱动回调最终产生。
2、输入案例实现
由上节分析可知,五向开关作为输入设备,需要定义其类型为LV_INDEV_TYPE_KEYPAD,并且编写读取回调,然后注册输入设备,最终的main.c代码如下(基于Luckfox官方案例lvgl_demo和本人上一篇测评,仅修改main.c):
#include "lvgl/lvgl.h"
#include "DEV_Config.h"
#include "lv_drivers/display/fbdev.h"
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
/**
by Firr. 五向按键做LVGL输入设备
*/
// 按键读取函数——返回按键对应IO编号以区分不同的按键
uint8_t getKeyValue(void) {
if(GET_KEY_RIGHT == 0) return KEY_RIGHT_PIN;
else if(GET_KEY_LEFT == 0) return KEY_LEFT_PIN;
else if(GET_KEY_UP == 0) return KEY_UP_PIN;
else if(GET_KEY_DOWN == 0) return KEY_DOWN_PIN;
else if(GET_KEY_PRESS == 0)return KEY_PRESS_PIN;
else return 0;
}
// 自定义输入事件接口——输入读取回调
void keypadRead(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
static uint32_t last_key = 0;
// 读取按键值——即按键对应IO编号,作为“输入事件的数值”
uint32_t act_key = getKeyValue();
if(act_key != 0) {
// 有按键按下,则修改“输入事件的状态”
data->state = LV_INDEV_STATE_PR;
/* 转换按键值为“LVGL控件字符(LVGL control characters)” */
switch(act_key) {
case KEY_LEFT_PIN:
act_key = LV_KEY_LEFT; // 减少值或向左移动
break;
case KEY_RIGHT_PIN:
act_key = LV_KEY_RIGHT; // 减少值或向右移动
break;
case KEY_UP_PIN:
act_key = LV_KEY_PREV; // 聚焦到上一对象
break;
case KEY_DOWN_PIN:
act_key = LV_KEY_NEXT; // 聚焦到下一对象
break;
case KEY_PRESS_PIN:
act_key = LV_KEY_ENTER; // 触发LV_EVENT_PRESSED/CLICKED/LONG_PRESSED
break;
}
last_key = act_key;
} else {
data->state = LV_INDEV_STATE_REL;
}
// 将不同按键转化为LVGL定义的“输入事件key”
data->key = last_key;
}
// LVGL显示缓存区及显示回调声明
#define DISP_BUF_SIZE (160 * 128)
void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);
// 按键设备指针——用于设置给“按键设备”设置“(焦点)组”
lv_indev_t *indev_keypad;
// 按键回调函数
void event_handler(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) printf("Clicked\n");
else if(code == LV_EVENT_VALUE_CHANGED) printf("Toggled\n");
}
// 构建两个按键的UI
void btnExample(void) {
// 按键上的文本
lv_obj_t * label;
// 创建按键btn1——父元素系统屏幕(lv_scr_act()返回)
lv_obj_t * btn1 = lv_btn_create(lv_scr_act());
// 按键btn1具备“可选中(focused)”属性
lv_obj_add_flag(btn1, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
lv_obj_clear_flag(btn1, LV_OBJ_FLAG_SCROLLABLE);
// 按键btn1绑定事件(全部事件)回调
lv_obj_add_event_cb(btn1, event_handler, LV_EVENT_ALL, NULL);
// 按键btn1位于父元素(系统屏幕)中心向左40px位置
lv_obj_align(btn1, LV_ALIGN_CENTER, -40, 0);
// 按键btn1的文本“Btn”,且位于按键中心
label = lv_label_create(btn1);
lv_label_set_text(label, "Btn");
lv_obj_center(label);
// 创建按键btn2,位于屏幕中心向右40px位置,且可选中(选中后背景色改变)
lv_obj_t * btn2 = lv_btn_create(lv_scr_act());
// 按键btn2具备“可选中(focused)”属性
lv_obj_add_flag(btn2, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
lv_obj_clear_flag(btn2, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(btn2, event_handler, LV_EVENT_ALL, NULL);
lv_obj_align(btn2, LV_ALIGN_CENTER, 40, 0);
lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_height(btn2, LV_SIZE_CONTENT);
// 按键btn2文本“Toggle”
label = lv_label_create(btn2);
lv_label_set_text(label, "Tog");
lv_obj_center(label);
/* 创建组——界面中所有可选中的控件应以“组”进行管理 */
lv_group_t* Key_group;
Key_group = lv_group_create(); // 创建组实例
lv_indev_set_group(indev_keypad, Key_group); // 组-绑定-输入设备
lv_group_add_obj(Key_group, btn1); // btn1加入组
lv_group_add_obj(Key_group, btn2); // btn2加入组
lv_group_set_editing(Key_group, false); // 组内不可编辑
}
int main(void)
{
/*LittlevGL init*/
lv_init();
/*Linux frame buffer device init*/
fbdev_init();
/*A small buffer for LittlevGL to draw the screen's content*/
static lv_color_t buf[DISP_BUF_SIZE];
/*Initialize a descriptor for the buffer*/
static lv_disp_draw_buf_t disp_buf;
lv_disp_draw_buf_init(&disp_buf, buf, NULL, DISP_BUF_SIZE);
/*Initialize and register a display driver*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &disp_buf;
disp_drv.flush_cb = fbdev_flush;
disp_drv.hor_res = 160;
disp_drv.ver_res = 128;
lv_disp_drv_register(&disp_drv);
/*Initialize pin*/
DEV_ModuleInit();
/* lvgl输入初始化 */
static lv_indev_drv_t indev_drv; // 输入设备实例初始化
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_KEYPAD;
indev_drv.read_cb = keypadRead; // 输入事件回调
indev_keypad = lv_indev_drv_register(&indev_drv); // 注册输入设备并获取返回指针
// 显示应用UI
btnExample();
/*Handle LitlevGL tasks (tickless mode)*/
while(1) {
lv_timer_handler();
usleep(5000);
}
return 0;
}
/*Set in lv_conf.h as `LV_TICK_CUSTOM_SYS_TIME_EXPR`*/
uint32_t custom_tick_get(void)
{
static uint64_t start_ms = 0;
if(start_ms == 0) {
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
}
struct timeval tv_now;
gettimeofday(&tv_now, NULL);
uint64_t now_ms;
now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;
uint32_t time_ms = now_ms - start_ms;
return time_ms;
}
为了测试输入功能,本例在界面上构建两个按钮“Btn”和“Tog”,因为使用开关作为输入而不是鼠标或触屏等Pointer类设备,所以能够交互的控件必须设置“可选中(focused)”属性,同时Tog按键还设置了“可Check”属性——按钮点击后会切换Checked状态并变色。而LVGL又要求可选中的控件必须放置在“组”中进行管理。
案例效果是拨动上、下开关可以圈选不同的按钮,按下开关可以点击圈中按钮。两个按钮点击后触发回调进行控制台输出,
注:除了_lv_timer_ll,LVGL还有不少全局数据结构,不过其定义源码都是多次参数宏嵌套而成的,有兴趣的朋友可以查看“lvgl/src/misc/lv_gc.h、lvgl/src/misc/lv_gc.c”这两个文件。
[localvideo]4689abdc3f9aff04f8f108e15dad3d11[/localvideo]
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】一、开箱及测试
slotg 发表于 2024-2-20 14:10
请问你的ssh可以连的上吗?
按照文档(https://wiki.luckfox.com/zh/Luckfox-Pico/SSH-Telnet-Login)进行配网的话,可以利用SSH连接,我实践过。评测测试时只是觉得用ADB省事。
- 2024-02-19
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十二、实现FrameBuffer设备及LVGL应用
LitchiCheng 发表于 2024-2-19 09:34
这个屏幕什么价格,不贵的话我也搞一个,显示起来还挺方便
合宙的,10几块钱吧,其实还不如买微雪的,官方案例可以适配,尺寸也大点。
- 2024-02-18
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十二、实现FrameBuffer设备及LVGL应用
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
十一、SPI驱动LCD
本篇测评基于上一测评中开启的SPI0接口,进一步通过修改设备树文件<SDK目录>/sysdrv/source/kernel/arch/arm/boot/dts/rv1106g-luckfox-pico-pro-max.dts,使得Luckfox Pro Max驱动(SPI0口)的0.96寸合宙LCD(ST7735)注册为FrameBuffer设备。这样,LVGL就可以基于FB作为显示接口。
本次实验主要参考Luckfox Wiki之LVGL使用指南篇(https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-LVGL)。
1、设备树文件修改
官方文档中提供的LVGL.zip包含系统镜像、lvgl_demo工程和设备文件,可惜是基于微雪Pico-LCD-1.3显示屏(ST7789驱动),与本人手中的合宙LCD不同。好在两款LCD驱动器都是Sitronix公司的产品,而且Linux系统中都有驱动。
微雪1.3寸屏附带五向开关和ABXY四按键,所以LVGL.zip中的设备树文件除了SPI0的配置还有这些按键控制IO的配置,因为用到IO多,所以I2C3等接口都被禁用了。本人使用的合宙0.96寸屏只附带五向开关,所以没有直接拷贝设备树文件,只是修改了SPI0部分。完整的rv1106g-luckfox-pico-pro-max.dts文件代码如下:
// 基于官方案例提供设备树源码,剔除注释部分和未使用IO部分
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
* Copyright (c) 2022 Rockchip Electronics Co., Ltd.
*/
/dts-v1/;
#include "rv1106.dtsi"
#include "rv1106-evb.dtsi"
#include "rv1106-luckfox-pico-pro-max-ipc.dtsi"
/ {
model = "Luckfox Pico Max";
compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1106";
/*KEY*/
/*KEY-DOWN*/
gpio2pa0:gpio2pa0 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio2_pa0>;
regulator-name = "gpio2_pa0";
regulator-always-on;
};
/*KEY-RIGHT*/
gpio2pa2:gpio2pa2 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio2_pa2>;
regulator-name = "gpio2_pa2";
regulator-always-on;
};
/*KEY-LEFT*/
gpio2pa4:gpio2pa4 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio2_pa4>;
regulator-name = "gpio2_pa4";
regulator-always-on;
};
/*KEY-CENTER*/
gpio1pc6:gpio1pc6 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio1_pc6>;
regulator-name = "gpio1_pc6";
regulator-always-on;
};
/*KEY-UP*/
gpio1pc7:gpio1pc7 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio1_pc7>;
regulator-name = "gpio1_pc7";
regulator-always-on;
};
/*LCD_RES*/
gpio1pc3:gpio1pc3 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio1_pc3>;
regulator-name = "gpio1_pc3";
regulator-always-on;
};
/*LCD_BL*/
gpio2pb0:gpio2pb0 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio2_pb0>;
regulator-name = "gpio2_pb0";
regulator-always-on;
};
/*LCD_DC*/
gpio2pb1:gpio2pb1 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio2_pb1>;
regulator-name = "gpio2_pb1";
regulator-always-on;
};
};
/**********GPIO**********/
&pinctrl {
/*KEY*/
gpio2-pa0 {
gpio2_pa0:gpio2-pa0 {
rockchip,pins = <2 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
gpio2-pa2 {
gpio2_pa2:gpio2-pa2 {
rockchip,pins = <2 RK_PA2 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
gpio2-pa4 {
gpio2_pa4:gpio2-pa4 {
rockchip,pins = <2 RK_PA4 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
gpio1-pc6 {
gpio1_pc6:gpio1-pc6 {
rockchip,pins = <1 RK_PC6 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
gpio1-pc7 {
gpio1_pc7:gpio1-pc7 {
rockchip,pins = <1 RK_PC7 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
/*RESET*/
gpio1-pc3 {
gpio1_pc3:gpio1-pc3 {
rockchip,pins = <1 RK_PC3 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
/*BL*/
gpio2-pb0 {
gpio2_pb0:gpio2-pb0 {
rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
/*LCD_DC*/
gpio2-pb1 {
gpio2_pb1:gpio2-pb1 {
rockchip,pins = <2 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
/**********FLASH**********/
&sfc {
status = "okay";
flash@0 {
compatible = "spi-nand";
reg = <0>;
spi-max-frequency = <75000000>;
spi-rx-bus-width = <4>;
spi-tx-bus-width = <1>;
};
};
/**********ETH**********/
&gmac {
status = "okay";
};
/**********USB**********/
&usbdrd_dwc3 {
status = "okay";
dr_mode = "peripheral";
};
/**********I2C**********/
&i2c3 {
status = "okay";
pinctrl-0 = <&i2c3m1_xfer>;
clock-frequency = <100000>;
};
/**********SPI**********/
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0m0_cs0 &spi0m0_pins>;
st7735s@0{
status = "okay";
compatible = "sitronix,st7735r";
reg = <0>;
spi-max-frequency = <48000000>;
// width = <80>;
// height = <160>;
rotate = <270>;
fps = <30>;
buswidth = <8>;
debug = <0x7>;
led-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_HIGH>;//BL
dc = <&gpio2 RK_PB1 GPIO_ACTIVE_HIGH>; //DC
reset = <&gpio1 RK_PC3 GPIO_ACTIVE_LOW>; //RES
};
};
&pinctrl {
spi0 {
/omit-if-no-ref/
spi0m0_pins: spi0m0-pins {
rockchip,pins =
/* spi0_clk_m0 */
<1 RK_PC1 4 &pcfg_pull_none>,
/* spie_miso_m0 */
/* <1 RK_PC3 6 &pcfg_pull_none>, */
/* spi_mosi_m0 */
<1 RK_PC2 6 &pcfg_pull_none>;
};
};
};
除了设备树文件,还要修改<SDK目录>/sysdrv/source/kernel/arch/arm/configs/luckfox_rv1106_linux_defconfig,以添加FB文件支持。直接编辑文件可以省去make menuconfig的步骤。
图12-1 FB设备支持添加项
设备树和配置文件修改后,就可以再次编译内核并烧写开发板了。更新系统镜像后再次启动开发板,就可以看到设备文件:/dev/fb0。
图12-2 开发板具备了fb0
2、lvgl_demo项目修改
官方提供的LVGL.zip中包含测试项目lvgl_demo,将项目文件夹拷贝到虚拟机,因为项目是基于FB的LVGL移植案例,所以不用再做其它修改,只需要更新Makefile中CC变量的赋值为Luckfox SDK路径。
另外,合宙屏在使用案例时一上来控制台输出报错信息:ioctl(FBIOBLANK): Invalid argument,参考一篇技术贴(https://blog.csdn.net/klp1358484518/article/details/130032766),需要注释掉lv_drivers/display/fbdev.c中的几行代码:
// 位于fbdev_init()函数
// Make sure that the display is on.
// if (ioctl(fbfd, FBIOBLANK, FB_BLANK_UNBLANK) != 0) {
// perror("ioctl(FBIOBLANK)");
// return;
// }
当然,由于本人使用的是0.96寸屏,所以main.c做了修改——原案例显示图片适配1.3寸屏,这里改为屏幕中心显示一行字符串。main.c的代码如下:
#include "lvgl/lvgl.h"
#include "DEV_Config.h"
#include "lv_drivers/display/fbdev.h"
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#define DISP_BUF_SIZE (160 * 128)
void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);
int main(void)
{
/*LittlevGL init*/
lv_init();
/*Linux frame buffer device init*/
fbdev_init();
/*A small buffer for LittlevGL to draw the screen's content*/
static lv_color_t buf[DISP_BUF_SIZE];
/*Initialize a descriptor for the buffer*/
static lv_disp_draw_buf_t disp_buf;
lv_disp_draw_buf_init(&disp_buf, buf, NULL, DISP_BUF_SIZE);
/*Initialize and register a display driver*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &disp_buf;
disp_drv.flush_cb = fbdev_flush;
disp_drv.hor_res = 160;
disp_drv.ver_res = 128;
lv_disp_drv_register(&disp_drv);
/*Initialize pin*/
DEV_ModuleInit();
lv_obj_t *scr = lv_disp_get_scr_act(NULL);
// 显示字符串
lv_obj_t *label = lv_label_create(scr);
lv_label_set_text(label, "Luckfox LVGL FB!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
/*Create a cursor*/
lv_obj_t *cursor = lv_img_create(scr);
lv_img_set_src(cursor, LV_SYMBOL_GPS);
lv_obj_set_pos(cursor, 70, 40);
int x=70,y=40,move=0;
/*Handle LitlevGL tasks (tickless mode)*/
while(1) {
lv_timer_handler();
usleep(5000);
/*Joystick*/
if(GET_KEY_RIGHT == 0){
x += 1;
if(x > 226)x = 226;
move =1;
}
else if(GET_KEY_LEFT == 0){
x -= 1;
if(x < 0)x = 0;
move =1;
}
else if(GET_KEY_UP == 0){
y -= 1;
if(y < 0)y = 0;
move =1;
}
else if(GET_KEY_DOWN == 0){
y += 1;
if(y > 224)y = 224;
move =1;
}
else if(GET_KEY_PRESS == 0){
x = 80;
y = 64;
move =1;
}
if(move == 1){
lv_obj_set_pos(cursor, x, y);
move = 0;
}
}
return 0;
}
/*Set in lv_conf.h as `LV_TICK_CUSTOM_SYS_TIME_EXPR`*/
uint32_t custom_tick_get(void)
{
static uint64_t start_ms = 0;
if(start_ms == 0) {
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
}
struct timeval tv_now;
gettimeofday(&tv_now, NULL);
uint64_t now_ms;
now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;
uint32_t time_ms = now_ms - start_ms;
return time_ms;
}
图12-3 案例效果——五向开关拨动屏幕上的图标也会移动
3、调试中的问题说明
项目看似简单但也耗费了在下一天的时间进行调试,主要是屏幕适配的问题,最初屏幕是纵向显示的(即上图字体左旋90°)。而本人又将调整屏幕方向的思路放在修改程序代码上,使用lv_disp_set_rotation()函数,但是没有成功,判断就是底层驱动不支持旋转屏幕,于是转思路尝试修改设备文件。注:后续发现LVGL文档中提到如果没有硬件支持,可以使用lv_draw_sw_rotate()函数进行转屏,不过自己没有进行尝试,也不清楚是否有效。
图12-4 LVGL在线文档Rotation部分页面截图
关于ST7735设备树节点描述的资料不太好找,本人也是搜索了几篇技术博客,从中分析到“旋转、宽高”属性的设置方法。不过经过验证,屏幕尺寸如果依照0.98屏的真实参数设置会出现部分屏幕不能使用的问题,索性只设置了rotate。
而关于宽高设置后出现问题的原因,个人猜测可能是Linux源码中提供的ST7735r驱动就是适配160*128尺寸的,不能兼容其它尺寸。这一点也是查看了Linux源码 Documentation中的相应说明文档分析出来的——<Luckfox SDK>/sysdrv/source/kernel/Documentation/devicetree/bindings/display/sitronix,st7735r.yaml。
图12-5 合宙0.96寸屏的设备树节点
图12-6 设置了宽高属性的效果——屏幕一部分无法使用
图12-7 sitronix,st7735r.yaml文档内容截图
上述系统镜像烧写后,开发板中通过命令fbset,可以看到当前屏幕分辨率就是160x128——关于真实屏幕参数为160x80系统却可以识别为160x128在下也是觉得蛮魔性的。有鉴于此,在下也是在程序中设置屏幕尺寸为160x128,没想到还真能准确显示了。
图12-8 fbset命令查看屏幕分辨率
图12-9 LVGL设置宽高依据系统分辨率
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十一、SPI驱动LCD
LitchiCheng 发表于 2024-2-17 16:21
我手上有两个spi的屏幕,没资料了可惜了,也不是通用,要不然也可以试试
也是实验中刚学到的:<Luckfox SDK>/sysdrv/source/kernel/Documentation/devicetree/bindings/display,这个路径下有支持的屏幕驱动的说明文档,你可以看看有没有符合自己的。
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】九、Python控制I2C驱动OLED
LitchiCheng 发表于 2024-2-17 16:20
楼主的探究精神很强!又去找了下具体的原因
谢谢夸奖,也是随意浏览了一下。
- 2024-02-16
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十一、SPI驱动LCD
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
本篇记录Luckfox Pro Max板通过SPI0驱动0.96寸TFT LCD(ST7735驱动)的实验过程,以官方提供的C代码进行验证,并以此项目为基础整合LVGL。不过,因为不是基于Linux FrameBuffer接口,而是纯以本人的MCU思维构建的以官方案例提供的LCD驱动作为底层显示接口的方式,所以最终效果不甚理想,仅作为实验参考吧。
1、官方案例验证
官方Wiki之SPI驱动LCD篇(https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-LCD),正好是采用0.96寸LCD且ST7735驱动,符合本人手中的合宙LCD扩展板。
官方案例采用的微雪Pico-LCD-0.96模块,除了屏幕还有五向开关和AB按键,而合宙LCD板只有五向开关,而且接线顺序不同,稍花功夫按着文档的接线图才做好连线。
图11-1 官方采用的微雪Pico-LCD-0.96
图11-2 本人采用的合宙0.96寸扩展板
图11-3 Pro Max和合宙扩展板的最终连线(PA_1~PB_11对应扩展板五向开关)
官方提供了测试案例,压缩包中包含不同开发板的项目,这里选择Luckfox_Pico_ProMax目录,其中有程序案例(c文件夹)和设备树文件。
图11-4 官方案例中的项目和设备树文件
首先是需要将设备树文件拷贝到虚拟机中,替换<SDK目录>/sysdrv/source/kernel/arch/arm/boot/dts/rv1106g-luckfox-pico-pro-max.dts,然后回到Luckfox SDK根目录,进行系统镜像编译,即:./build.sh lunch --> ProMax板需要选择8 --> ./build.sh。
将编译后的系统镜像重新下载到开发板中,此时开发板开启了SPI0,以及其它控制LCD用的GPIO(除了SCK和SDA外,其它9个信号都通过GPIO控制,因为官方案例还要驱动AB键,所以实际用到了11个GPIO)。
图11-5 更新系统后开发板开启的GPIO
然后就是将官方案例拷贝到虚拟机中,整个项目比较完善可以测试多款LCD(当然都是微雪家的),也只需要修改Makefile中的CC赋值为Luckfox SDK中的工具链即可。修改后就是make,然后项目根目录生成可执行文件main,接着就是下载本机再adb push到开发板,chmod赋执行权限,最后:“./demo 0.96”进行验证——表示执行0.96寸屏Demo。注:因为Demo有显示图片的功能,所以需要将项目的pic目录也推送到开发板。
图11-6 官方LCD测试项目结构
图11-7 官方LCD测试项目的Makefile修改
图11-8 至少把pic目录和执行文件main一起推送开发板
2、案例项目直接整合LVGL
Linux环境下搞LVGL,当然推荐采用FB接口——无论是LVGL官方的移植案例还是Luckfox官方案例都是如此。不过本人前一段时间刚做过ESP32上的LVGL移植,所以忍不住用MCU的思想搞了一下移植测试。
整合项目的初始阶段,本人计划是修改原案例Makefile,添加LVGL的编译内容,但是LVGL项目结构明显更复杂,再加上自己Makefile不熟悉,搞了一上午也没有成功。于是转向采用lv_port_linux_frame_buffer(LVGL官方提供的Linux移植版本)的Makefile为基础整合Luckfox的LCD案例的方式。
首先,虚拟机中重新拷贝一份Luckfox LCD案例中的“c”文件夹并重命名为pico_lvgl,将examples/main.c移动到根目录。同时,新建文件夹pico_lvgl/src,把examples和lib目录移到src目录下。
接着,在pico_lvgl目录下,执行命令:http://git clone -b v8.1.0 https://github.com/lvgl/lvgl.git,克隆一份LVGL 8.1版源码。拷贝lvgl/lv_conf_template.h到pico_lvgl根目录并重命名为“lv_conf.h”,同时修改其中的“#if 0”为“#if 1”即使能LVGL的各个配置项——这里没有对配置项做修改。
然后,修改pico_lvgl/Makefile,即根目录下的主Makefile。其中代码以lv_port_linux_frame_buffer项目的Makefile为主稍作修改(增加Luckfox案例的编译规则等)。Makefile代码如下,中文注释部分是添加的内容。
#
# Makefile
#
# 请根据实际情况修改gcc路径
CC = ~/luckfox-pico/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
LVGL_DIR_NAME ?= lvgl
LVGL_DIR ?= .
WARNINGS := -Wall -Wshadow -Wundef -Wmissing-prototypes -Wno-discarded-qualifiers -Wall -Wextra -Wno-unused-function -Wno-error=strict-prototypes -Wpointer-arith \
-fno-strict-aliasing -Wno-error=cpp -Wuninitialized -Wmaybe-uninitialized -Wno-unused-parameter -Wno-missing-field-initializers -Wtype-limits -Wsizeof-pointer-memaccess \
-Wno-format-nonliteral -Wno-cast-qual -Wunreachable-code -Wno-switch-default -Wreturn-type -Wmultichar -Wformat-security -Wno-ignored-qualifiers -Wno-error=pedantic \
-Wno-sign-compare -Wno-error=missing-prototypes -Wdouble-promotion -Wclobbered -Wdeprecated -Wempty-body -Wtype-limits -Wshift-negative-value -Wstack-usage=2048 \
-Wno-unused-value -Wno-unused-parameter -Wno-missing-field-initializers -Wuninitialized -Wmaybe-uninitialized -Wall -Wextra -Wno-unused-parameter \
-Wno-missing-field-initializers -Wtype-limits -Wsizeof-pointer-memaccess -Wno-format-nonliteral -Wpointer-arith -Wno-cast-qual -Wmissing-prototypes \
-Wunreachable-code -Wno-switch-default -Wreturn-type -Wmultichar -Wno-discarded-qualifiers -Wformat-security -Wno-ignored-qualifiers -Wno-sign-compare -std=c99
CFLAGS ?= -O3 -g0 -I$(LVGL_DIR)/ $(WARNINGS)
# Pico LCD驱动相关头文件路径
CFLAGS += -I$(LVGL_DIR)/src/examples -I$(LVGL_DIR)/src/lib/Config -I$(LVGL_DIR)/src/lib/Fonts -I$(LVGL_DIR)/src/lib/GUI -I$(LVGL_DIR)/src/lib/LCD
LDFLAGS ?= -lm
BIN = demo
BUILD_DIR = ./build
BUILD_OBJ_DIR = $(BUILD_DIR)/obj
BUILD_BIN_DIR = $(BUILD_DIR)/bin
prefix ?= /usr
bindir ?= $(prefix)/bin
#Collect the files to compile
MAINSRC = ./main.c
# Pico LCD驱动相关.c文件
DRVSRC = $(shell find -L $(LVGL_DIR)/src -name \*.c)
include $(LVGL_DIR)/lvgl/lvgl.mk
OBJEXT ?= .o
AOBJS = $(ASRCS:.S=$(OBJEXT))
COBJS = $(CSRCS:.c=$(OBJEXT))
MAINOBJ = $(MAINSRC:.c=$(OBJEXT))
# Pico LCD驱动相关.o文件
DRVOBJ = $(DRVSRC:.c=$(OBJEXT))
SRCS = $(ASRCS) $(CSRCS) $(MAINSRC) $(DRVSRC)
OBJS = $(AOBJS) $(COBJS) $(MAINOBJ) $(DRVOBJ)
TARGET = $(addprefix $(BUILD_OBJ_DIR)/, $(patsubst ./%, %, $(OBJS)))
## MAINOBJ -> OBJFILES
all: default
$(BUILD_OBJ_DIR)/%.o: %.c
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) -c $< -o $@
[url=home.php?mod=space&uid=43340]@echo[/url] "CC $<"
default: $(TARGET)
@mkdir -p $(dir $(BUILD_BIN_DIR)/)
$(CC) -o $(BUILD_BIN_DIR)/$(BIN) $(TARGET) $(LDFLAGS)
clean:
rm -rf $(BUILD_DIR)
install:
install -d $(DESTDIR)$(bindir)
install $(BUILD_BIN_DIR)/$(BIN) $(DESTDIR)$(bindir)
uninstall:
$(RM) -r $(addprefix $(DESTDIR)$(bindir)/,$(BIN))
# 测试Makefile变量规则
show:
echo $(CFLAGS)
最后,修改pico_lvgl/main.c,即添加LVGL的测试代码——屏幕中间显示一行文本。main.c的代码如下:
#include "DEV_Config.h"
#include "Pico_LCD_0in96.h"
#include "GUI_Paint.h"
#include "GUI_BMP.h"
#include "lvgl/lvgl.h"
#include <signal.h>
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
// 定义屏幕尺寸
const uint16_t screenWidth = PICO_LCD_0IN96_WIDTH;
const uint16_t screenHeight = PICO_LCD_0IN96_HEIGHT;
// lvgl专用的显示缓存区定义
lv_disp_draw_buf_t draw_buf;
lv_color_t buf[ PICO_LCD_0IN96_WIDTH * PICO_LCD_0IN96_HEIGHT / 10 ];
// 自定义的显示刷新接口
void my_disp_flush( lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p )
{
// 调用Pico驱动提供的API有Bug
// PICO_LCD_0IN96_ClearWindow(area->x1, area->y1, area->x2, area->y2, (uint16_t*)color_p);
// 依据LVGL文档逐像素点刷新,很慢
uint16_t * buf16 = (uint16_t *)&color_p->full; /*Let's say it's a 16 bit (RGB565) display*/
int32_t x, y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
PICO_LCD_0IN96_DrawPaint(x, y, *buf16);
buf16++;
}
}
lv_disp_flush_ready( disp_drv );
}
int main(int argc, char *argv[]) {
// LVGL初始化
lv_init();
// Exception handling:ctrl + c
signal(SIGINT, Handler_0IN96_LCD);
/* Module Init */
if(DEV_ModuleInit() != 0){
DEV_ModuleExit();
exit(0);
}
/* LCD Init */
printf("0.96inch LCD demo...\r\n");
PICO_LCD_0IN96_Init();
PICO_LCD_0IN96_Clear(WHITE);
// 显示缓存区初始化
lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth*screenHeight/10);
// 显示初始化
static lv_disp_drv_t disp_drv;
lv_disp_drv_init( &disp_drv );
// 横屏设置尺寸
disp_drv.hor_res = screenWidth;
disp_drv.ver_res = screenHeight;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register( &disp_drv );
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello Luckfox LVGL!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
/*Handle LVGL tasks*/
while(1) {
lv_timer_handler();
usleep(5000);
}
return 0;
}
整合后的项目本人push到Gitee上,有感兴趣的朋友可以下载,因为需要再链接LVGL库(链接地址也是Fork到Gitee上的版本),需要使用命令如下:
git clone --recursive https://gitee.com/sonicfirr/pico_lvgl.git
整个项目还有一个问题就是刷新率非常低,以致没有实用价值。具体原因也没有找到,猜想可能跟编写的LVGL显示回调(my_disp_flush函数)性能不佳有关。由此,也没有做LVGL的输入测试,下一步还是直接采用Linux的FrameBuffer接口——关键官方也给了案例可以参考哈!
图11-9 lvgl_pico项目结构
- 2024-02-15
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】八、ADC和UART测试
lugl4313820 发表于 2024-2-15 15:44
他的ADC是多少位的呀?看起来好象非常准一样。
手册说明ADC电压范围是0~1.8V,读数最大1023,应该是10位吧
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】十、C程序控制I2C驱动OLED
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
本篇继续Luckfox Pro Max板I2C接口驱动0.96寸OLED(控制芯片SSD1306),以C代码进行控制。本人共测试了四个项目,两个成功(只需少量修改Makefile),两个失败(失败原因不明)。
1、成功案例——ssd1306_for_linux
这是Gitee上找到的案例(https://gitee.com/xianleewu/ssd1306_for_linux),作者在Orange Pi上进行了验证,从其README中分析是基于Armbian系统,本来不报太大希望,没想到一次验证就通过了。
案例使用过程也比较简单,首先是在Ubuntu虚拟机上进行项目clone:
git clone https://gitee.com/xianleewu/ssd1306_for_linux.git
然后,就是进入克隆好的项目目录,修改Makefile和“src/example.c”:
图10-1 ssd306_for_linux项目的Makefile和example.c修改
上述两处修改Makefile指定使用的编译工具由Luckfox SDK提供,然后源程序的修改指定使用I2C3接口。接着,就是make进行构建,在build目录下得到可执行程序“OrangePI_ssd1306”。
图10-2 ssd306_for_linux的构建结果
最后,将“OrangePI_ssd1306”拷贝到本机Win10,再adb push到开发板,chmod修改执行权限,然后运行验证即可。
图10-3 ssd306_for_linux运行效果
ssd1306_for_linux项目的测试案例是显示系统的内存、CPU使用率、CPU温度和日历——Luckfox板子没有联网进行授时所以本地时间获取不准。
2、成功案例——u8g2-arm-linux
图10-4 ssd306_for_linux项目的功能函数
上图可以看到ssd1306_for_linux项目提供的功能函数较少,只有“画点、画线、画方、写字”等,于是本人又想到使用经典的u8g2库。
Gitee上搜到一个Linux下的u8g2编译成lib的仓库,不过没有实验成果(失败案例之一),不过经仓库的README提示又找到Github上的项目源码仓库(https://github.com/wuhanstudio/u8g2-arm-linux)。
可是本人一贯访问Github的经历都不是很愉快,网速感人,好在已经找到了通过Gitee fork项目的本领,有需要的朋友也可以访问本人fork版本:https://gitee.com/sonicfirr/u8g2-arm-linux 。
项目依然是clone到虚拟机,修改Makefile和源码,然后下载本机推送到开发板执行的过程。本人测试的是项目自带的C案例,硬I2C版本(路径/u8g2-arm-linux/examples/c-examples/u8g2_hw_i2c)。
图10-5 u8g2-arm-linux项目的修改
图10-6 u8g2-arm-linux项目构建程序位于“bin”目录
图10-7 u8g2-arm-linux项目测试案例效果
3、失败案例——u8g2-arm-linux-lib和微雪RPI项目移植
失败案例之一就是Gitee上先找到u8g2 Lib(https://gitee.com/haywire/u8g2-arm-linux-lib)库,没有花太多功夫调试,只是在编译时报错有关于IO控制的函数没有定义,猜想可能是u8g2-arm-linux原项目中使用了“c-periphery”库作为底层接口驱动,而这个Lib库没有做好c-periphery的打包。当然更有可能就是使用方法不当——这里本人很粗略的将u8g2_hw_i2c.c的代码直接拷贝出来尝试连接这个Lib进行编译。
失败案例之二耗费了在下两天的时间,最终也没有成功,这里简述一下尝试的流程。
因为Luckfox官方提供的OLED案例就是基于微雪电子的1.3寸OLED,驱动芯片SH1107,项目结构如下。个人经过分析,在c/lib/OLED目录中创建了0.96寸OLED的驱动源文件,其中的代码则是尝试移植了微雪提供的0.96寸OLED案例(https://www.waveshare.net/wiki/0.96inch_OLED_(A))。
第一次尝试没有成功,于是转向微雪的0.96寸的案例直接进行移植修改,依然失败——屏幕显示了雪花点。微雪的案例是基于树莓派的,最后本人干脆把树莓派拿出来进行测试,结果只显示了第一个画面“时钟”,后续画面都没有显示出来——分析其案例应该还有四个画面。所以这里判断案例本身有一定问题,也可能是案例中的功能需要获取画面数据的部分在Luckfox上失效了。
微雪的树莓派版案例底层驱动可以基于BCM2835、wiringPi或lgpio三个库来实现,移植到Luckfox则只能改为访问设备文件。目前本人判断I2C3接口访问是成功的,OLED也可以点亮,应该还是GUI功能函数不兼容。考虑到时间耗费较久了,也就放弃继续调试,准备下一步转向SPI驱动LCD的方向。
图10-8 Luckfox官方的OLED案例项目
- 2024-02-13
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】九、Python控制I2C驱动OLED
测评跟踪报告:在本篇完成后,在继续分析Luckfox Pro Max设备文件中发现,另一个文件“RV106-luckfox-pico-pro-max-ipc.dtsi”中,有关于I2C4的设备节点,看其代码应该被用作摄像头接口了。通过本人查看,当开启了I2C4后,摄像头果然不能用了,请看过本篇的朋友还是使用I2C3吧。设备文件的代码片段:
&i2c4 {
status = "okay";
clock-frequency = <400000>;
pinctrl-names = "default";
pinctrl-0 = <&i2c4m2_xfer>;
sc3336: sc3336@30 {
compatible = "smartsens,sc3336";
status = "okay";
reg = <0x30>;
clocks = <&cru MCLK_REF_MIPI0>;
clock-names = "xvclk";
pwdn-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&mipi_refclk_out0>;
rockchip,camera-module-index = <0>;
rockchip,camera-module-facing = "back";
rockchip,camera-module-name = "CMK-OT2119-PC1";
rockchip,camera-module-lens-name = "30IRC-F16";
port {
sc3336_out: endpoint {
remote-endpoint = <&csi_dphy_input0>;
data-lanes = <1 2>;
};
};
};
}
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】九、Python控制I2C驱动OLED
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
本篇测评以Python程序控制Luckfox Pro Max的I2C接口驱动0.96寸OLED,OLED的控制器为SSD1306。
官方提供的Python程序是控制1.5寸OLED的,驱动芯片是SSD1327。于是索性在Gitee上找了一个开源项目:https://gitee.com/wygrgzs/python_ssd1306/tree/master 。此项目应该是作者移植AdaFruit(README没有说,本人是通过源码注释分析其出处)的Micropython版驱动而来。
1、注意避坑
PWM的测评环节依照Luckfox文档启用了PWM0,由于管脚冲突,所以当时在设备文件中禁用了I2C3接口,而I2C文档中案例都是使用I2C3接口的。不过本人通过命令查看I2C设备文件,得到如下结果:
图9-1 查看I2C设备文件
于是,便想到不如使用I2C4来驱动OLED,结果无论是i2cdetect等命令或者Python程序都没有成功,并且Python程序报错OSError 6,经过查询这是smbus(Python程序使用的库)的设备未找到错误。由此,本人想到I2C4接口应该没有启用。
之后,翻看SDK中的设备文件源码,有如下内容:
图9-2 Pro Max I2C相关设备文件
可以看到开发板的设备文件“rv1106g-luckfox-pico-pro-max.dts”的起始位置通过#include又导入了“rv1106.dtsi”文件。这个文件应该是RV1106的主设备树文件,定义了大量的外设接口,不过所有接口都是禁用状态(status = "disabled"),然后在rv1106g-luckfox-pico-pro-max.dts中可以将用到接口再启用。
于是,重新修改rv1106g-luckfox-pico-pro-max.dts文件,禁用PWM0(和I2C管脚冲突),启用I2C3和I2C4。然后就是重新编译内核,并烧写到开发板中。
图9-3 Pro Max 启用I2C3和I2C4
2、Python控制程序
Python测试代码就使用Gitee案例,只修改了案例ssd1306.py的最后入口代码:
if __name__=='__main__':
i2c = smbus.SMBus(4) # 启用I2C4,或者传参3即启用I2C3都可以
oled=SSD1306_I2C(128,64,i2c) # 0.96寸,分辨率128*64
oled.fill(0)
oled.text("Luckfox", 0, 0 , 1) # 案例测试控制OLED显示两行英文
oled.text("Hello Pro Max", 0, 8 , 1)
oled.contrast(10)
oled.show()
原案例驱动的128*32的OLED,这里需要修改成128*64,案例只提供了一种英文字体,字体库由项目路径下的“font5x8.bin”文件提供。
图9-4 OLED显示效果
- 2024-02-07
-
发表了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】八、ADC和UART测试
本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
本篇继续测试Luckfox Pro Max板子的外围接口:ADC和UART——只测试了UART3。前面已经依照文档测评了GPIO和PWM输出,证明文档没有什么坑,所以本篇直接将ADC和UART3的控制合并到一起,编写一个“UART3接收命令,然后根据命令开启一次ADC转换”的实验程序。
图8-1 UART3连接和ADC管脚
板子串口启用UART3,19脚Tx,20脚Rx,有两个ADC(0/1)为31,32脚。串口通过USB-TTL连接到PC机,串口调试工具使用XCOM。
开发板的/sys/bus/iio/devices/iio\:device0/目录下是ADC设备的属性文件,主要操作三个:in_voltage_scale用于获取ADC读数和实际电压的转换因子,in_voltage0_raw用于获取ADC通道0读数,in_voltage1_raw用于获取ADC通道1读数。
开发板的/dev目录下包含UART相关的设备文件,UART3对应文件/dev/ttyS3。
1、Python方式
Python控制程序中,ADC没有使用库,直接进行文件操作,UART使用了serial库,需要注意的串口的读写函数对应参数都是bytes类型,尤其是调用serial.write()进行串口输出时,输出内容是字符串需要通过encode()函数转为bytes。具体代码如下:
import serial # 导入串行通信库
import time # 导入时间库,用于延时
# 初始化串行端口uart3,配置其参数
uart3 = serial.Serial(
"/dev/ttyS3", # 串行端口的设备文件路径
baudrate=115200, # 波特率,即每秒传输的位数
bytesize=serial.EIGHTBITS, # 数据位大小,这里是8位
stopbits=serial.STOPBITS_ONE, # 停止位数量,这里是1位
parity=serial.PARITY_NONE, # 校验位,这里不使用校验
timeout=1, # 读取超时时间,单位是秒
)
# ADC设备的路径
ADC_DIR = "/sys/bus/iio/devices/iio:device0"
# 定义用于从指定文件路径读取值的函数
def read_value(file_path):
with open(file_path, "r") as file: # 打开文件
return file.read().strip() # 读取文件内容并去除前后的空白字符
# 主函数
def main():
print("Press Ctrl+C to quit") # 提示用户如何退出程序
while True: # 无限循环
buf = uart3.read(128) # 从串行端口读取最多128个字节的数据
if b"ADC0" in buf: # 如果接收到的数据中包含"ADC0"
# 从ADC设备读取相关的值
scale_value = float(read_value(f"{ADC_DIR}/in_voltage_scale")) # 读取电压比例因子
IN0_raw_value = float(read_value(f"{ADC_DIR}/in_voltage0_raw")) # 读取ADC0的原始值
# 计算ADC0的电压值并格式化输出
IN0_voltage = f"{IN0_raw_value * scale_value / 1000:.2f}"
print(f"IN0_Voltage: {IN0_voltage} V") # 打印到控制台
uart3.write(f"IN0_Voltage: {IN0_voltage} V\n".encode('utf-8')) # 通过串行端口发送数据
elif b"ADC1" in buf: # 如果接收到的数据中包含"ADC1"
# 类似地,处理ADC1的数据
scale_value = float(read_value(f"{ADC_DIR}/in_voltage_scale"))
IN1_raw_value = float(read_value(f"{ADC_DIR}/in_voltage1_raw"))
IN1_voltage = f"{IN1_raw_value * scale_value / 1000:.2f}"
print(f"IN1_Voltage: {IN1_voltage} V")
uart3.write(f"IN1_Voltage: {IN1_voltage} V\n".encode('utf-8')) # encode转bytes
time.sleep(2) # 等待2秒
if __name__ == "__main__":
try:
main() # 调用主函数
except KeyboardInterrupt: # 如果用户按下Ctrl+C
uart3.close() # 关闭串行端口
pass # 什么都不做,继续退出程序
图8-2 adc.py执行效果
程序执行效果如上图所示,XCOM连接板子UART3,发送“ADC0”过去则控制开发板对通道0进行一次ADC读值,发送“ADC1”则控制通道1。这里为了省事,两个ADC管脚都是悬空的,看输出应该是悬空为“高”——看文档ADC管脚参考电压就是1.8V。
2、C方式
C语音程序也是以操作设备文件的方式进行ADC和UART控制,其中需要用到struct termios结构体,这个结构体是在 POSIX 规范中定义的一个标准接口,它类似于系统 V 中的 termio 接口。这个结构体用于控制非同步通信端口,提供了一个常规的终端接口。通过设置 termios 类型的数据结构中的值和使用一组函数调用,你可以对终端接口进行控制。而在 Linux 系统中,struct termios 结构体常用于串口编程,用于设置串口参数、读取和写入串口数据等操作。通过调整这个结构体的各个成员,你可以控制串口的各种属性,如波特率、数据位、停止位、校验位等,以及终端的输入输出行为。相关代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <termios.h>
// 定义一个串口发送函数,增加发送出错的判断
int writeBuf(int fd, char *buffer, int size) {
ssize_t bytes_written = write(fd, buffer, size);
if (bytes_written < 0) {
perror("Error writing to serial port");
close(fd);
return 1;
}
return 0;
}
int main() {
printf("Press Ctrl+C to quit\n");
// ADC设备文件的目录
const char *adc_dir = "/sys/bus/iio/devices/iio:device0";
// 定义三个字符数组,分别用于存储三个属性文件的路径
char in_voltage0_raw_path[256];
char in_voltage1_raw_path[256];
char in_voltage_scale_path[256];
// 字符数组赋值对应ADC的三个属性文件路径
sprintf(in_voltage0_raw_path, "%s/in_voltage0_raw", adc_dir);
sprintf(in_voltage1_raw_path, "%s/in_voltage1_raw", adc_dir);
sprintf(in_voltage_scale_path, "%s/in_voltage_scale", adc_dir);
// 打开三个ADC属性文件
FILE *scale_file = fopen(in_voltage_scale_path, "r");
FILE *in0_raw_file = fopen(in_voltage0_raw_path, "r");
FILE *in1_raw_file = fopen(in_voltage1_raw_path, "r");
// UART3设备文件路径
char serial_port[] = "/dev/ttyS3";
// 定义变量用于存储打开UART3后的文件符
int serial_fd;
// 打开UART3设备文件
serial_fd = open(serial_port, O_RDWR | O_NOCTTY);
if (serial_fd == -1) {
perror("Failed to open serial port");
return 1;
}
// 定义struct termios结构体,用于设置UART口参数
struct termios tty;
memset(&tty, 0, sizeof(tty));
// 从 serial_fd 所引用的设备中获取当前的终端属性
if (tcgetattr(serial_fd, &tty) != 0) {
perror("Error from tcgetattr");
return 1;
}
// 设置输入和输出波特率为115200
cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);
// 无奇偶校验、一个停止位、8 个数据位
tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
// 将串口属性值最终设置回文件
if (tcsetattr(serial_fd, TCSANOW, &tty) != 0) {
perror("Error from tcsetattr");
return 1;
}
// 串口收、发缓存和ADC读数缓存
char rx_buffer[64];
char tx_buffer[64];
char buffer[32];
float scale = 1.7578125;
while (1) {
int bytes_read = read(serial_fd, rx_buffer, sizeof(rx_buffer));
// 如果串口有接收
if (bytes_read > 0) {
rx_buffer[bytes_read] = '\0';
// 则先读取ADC的转换因子
fseek(scale_file, 0, SEEK_SET);
if (scale_file) {
fgets(buffer, sizeof(buffer), scale_file);
scale = strtof(buffer, NULL);
}
if (strstr(rx_buffer, "ADC0") != NULL) {
// 如果接收到“ADC0”则读取ADC通道0,并串3输出
fseek(in0_raw_file, 0, SEEK_SET);
if (in0_raw_file) {
fgets(buffer, sizeof(buffer), in0_raw_file);
int in0_raw_value = atoi(buffer);
float in0_voltage = (in0_raw_value * scale) / 1000.0;
printf("in ADC0 value: %d, volt: %.6f\n", in0_raw_value, in0_voltage);
memset(tx_buffer, 0, 64);
sprintf(tx_buffer, "IN0 Voltage: %.6f V\n", in0_voltage);
if (writeBuf(serial_fd, tx_buffer, strlen(tx_buffer)) != 0) return 1;
}
} else if (strstr(rx_buffer, "ADC1") != NULL) {
// 如果接收到“ADC1”则读取ADC通道1,并串3输出
fseek(in1_raw_file, 0, SEEK_SET);
if (in1_raw_file) {
fgets(buffer, sizeof(buffer), in1_raw_file);
int in1_raw_value = atoi(buffer);
float in1_voltage = (in1_raw_value * scale) / 1000.0;
printf("in ADC1 value: %d, volt:%.6f\n", in1_raw_value, in1_voltage);
memset(tx_buffer, 0, 64);
sprintf(tx_buffer, "IN1 Voltage: %.6f V\n", in1_voltage);
if (writeBuf(serial_fd, tx_buffer, strlen(tx_buffer)) != 0) return 1;
}
}
} else {
printf("No data received.\n");
}
sleep(1);
}
fclose(scale_file);
fclose(in0_raw_file);
fclose(in1_raw_file);
close(serial_fd);
return 0;
}
- 2024-02-06
-
回复了主题帖:
【Luckfox幸狐 RV1106 Linux 开发板测评】四、通过PC机共享网络接入Internet和Ubun...
沉默的羔羊1 发表于 2024-2-6 15:33
博主我烧录buildroot的时候烧录失败Error:Write LBA failed,can't read flash id from device!并且下载 ...
没遇到过,我这边测试还都比较顺利,抱歉