rk3399 | 通用驱动框架点灯测试
创始人
2024-06-03 14:33:43
0

1. 初识RK3399

RK3399是一款低功耗、高性能的处理器,适用于计算、个人移动互联网设备和其他智能设备应用。基于big.little架构,它集成了双核Cortex-A72和四核Cortex-A53与单独的NEON协处理器。
许多嵌入式强大的硬件引擎为高端应用程序提供了优化的性能。RK3399支持多种格式的视频解码器,包括H.264/H。265/VP9到4kx2k@60fps,特别是H.264/H。265解码器支持10bit编码,并通过1080p@30fps支持H.264/MVC/VP8编码器,高质量的JPEG编码器/解码器,以及特殊的图像预处理和后置处理器。

嵌入式3D GPU使RK3399完全兼容OpenGL ES1.1/2.0/3.0/3.1、OpenCL和DirectX 11.1。特殊的2D硬件引擎与MMU将最大化显示性能,并提供非常平稳的操作。
RK3399拥有高性能的双通道外存储器接口(DDR3/DDR3L/LPDDR3/LPDDR4),能够支持高要求的内存带宽,还提供了一套完整的外围接口来支持非常灵活的应用程序。

2. 测试框架介绍

最近在研究 linux 底层驱动框架,之前文章也学习了驱动通用框架以及输出总结相关socket通信的文章,刚好接触到rk3399 开发板,便着手实现 一个基于socket 通信的 ubuntu 和开发板交互的led 点灯的测试,具体框架如下:
在这里插入图片描述

ubuntu 上位机作为客户端,rk3399开发板最为服务端,不同客户端发送控制命令到服务端, 服务端根据已连接套接字区分客户端,然后解析控制命令驱动开发板底层led设备(/dev/led), 底层驱动通过open(“/dev/led”, O_RDWR)—>write(fd,…)系统调用接口调用驱动层的 led_write() 接口通过内核提供的copy_from_user/copy_to_user函数交互数据,以此实现客户端到开发板作为服务端的应用层再到驱动层以及设备外设驱动的整体驱动框架。

3. 驱动基础

驱动程序应用程序根据需求在驱动底层创建对应的驱动函数,创建好的接口函数应用层与驱动层一一对应,从面向对象的思想出发构造相应的结构体 file_operations,填充相应的结构体对象,然后将file_operations 结构体告诉内核 (注册file_operations结构体)。

内核是如何管理不同的驱动程序file_operations呢?

假设内核将相应的 file_operations 结构体以“数组”的形式存放在数组的第n项,那那如何知道内核数组的第n项是否被其他驱动占用,可以用regiister_chrdev函数中传入起始项编号0, 并将结构体file_operations 放入, regiister_chrdev函数便会遍历“数组”,返回一个未被占用的设备号n,此处n被称为主设备号(第几个字符设备)。有入口便有出口,字符驱动在退出时便会调用 unregiister_chrdev 函数释放相应的设备编号。

在这里插入图片描述
应用程序操作设备用 文件来完成, 内核要操作设备找驱动 用设备号来完成,写驱动本质就是将自己写的代码放到内核中去运行, 需要找内核要一个主设备号,关于驱动相关的详细介绍可参考之前文章 字符设备驱动基础 以及通用设备驱动框架。

4. 内核移植

内核源码:https://pan.baidu.com/s/1YxDAd1zd8jXVx89aVT1OaA?pwd=l45q

获取内核源码后进行如下配置

4.1 选择平台

robin@ubuntu:~/work/kernel-rockchip-nanopi4-linux-v4.4.y$ 
cp arch/arm64/configs/nanopi4_linux_defconfig .config

关于deconfig 文件的相关知识参考之前的文章 内核配置原理

4.2 内核自定义裁减

robin@ubuntu:~/work/kernel-rockchip-nanopi4-linux-v4.4.y$ make menuconfig

配置交叉工具链
在这里插入图片描述

配置版本信息

在这里插入图片描述

模块支持
在这里插入图片描述

4.3 内核编译

进入内核目录执行命令 make ARCH=arm64 nanopi4-images -j4 编译,编译完结果如下:

在这里插入图片描述

3. 刷机烧录

3.1 开发板连接

在这里插入图片描述

3.2 开发板刷机

一般刷机需要bootloader、kernel、rootfs三个镜像文件,具体如下图:
在这里插入图片描述
开发板串口调试界面如下:
在这里插入图片描述

切换开发板工作模式

root@NanoPC-T4:~# reboot loader

开发板进入 loader模式
在这里插入图片描述
打开瑞芯微烧录工具,待开发板进入loader 模式后,依次执行以下1~6部:
在这里插入图片描述
刷机完后的界面如下:
在这里插入图片描述

3.3 内核烧录

与刷机不同 rk3399 内核烧录只需将之前内核配置和自定义裁剪后的 Resource和Kernel 镜像文件烧写到开发板即可,烧录完后的界面如下:
在这里插入图片描述

5. 开发板驱动文件拷贝

驱动开发中若以模块驱动的方式加载驱动,在Ubuntu中开发编译完成后需将相关设备驱动文件拷贝至开发板,常用的方法有以下几种:

  • 局域网络:scp -->scp hello root@192.168.10.228:/drv_code
  • u盘 sd卡 挂载
  • 网络文件系统 nfs
  • adb push

6. 代码实例

ubuntu 客户端程序 cli.c

/*客户端循环收发一个字符串给服务端双向通信,服务端收发字符串, 注意在ubuntu运行只需gcc 编译即可*/#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include int fd;void * recv_mess(void *arg)
{char buf[50];int ret;pthread_detach(pthread_self()); //线程分离,不用主线程等待while (1) {bzero(buf, 50);ret = recv(fd, buf, sizeof(buf), 0);printf("client recv mess:%s",buf);printf("client ret=%d\n", ret);if(strncmp(buf, "quit", 4) == 0) exit(0);if(ret == 0) {printf("server offline\n");exit(0);}}return NULL;
}int main(void)
{int ret;//1.买电话fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1) {perror("socket");exit(1);}//2.绑卡#if 0struct sockaddr_in client;client.sin_family = AF_INET; //ipv4client.sin_port = htons(12345); //大端client.sin_addr.s_addr = inet_addr("192.168.10.251");ret = bind(fd, (struct sockaddr *)&client, sizeof(client));if (ret == -1) {perror("bind");exit(1);}#endif//3.打电话struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(12346);server.sin_addr.s_addr = inet_addr("192.168.10.228");ret = connect(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1) {perror("connect");exit(1);}system("netstat -an | grep 12346");	//查看tcp连接状态//创建线程pthread_t tid;pthread_create(&tid, NULL, recv_mess, NULL);//4.通信char buf[50] ;while (1) {bzero(buf, 50);printf("pls input cmd:\n");fgets(buf, 50, stdin);ret = send(fd, buf, strlen(buf), 0);//printf("send=%d\n", ret);if(strncmp(buf, "quit", 4) == 0) break;}//5.挂电话close(fd);return 0;
}

开发板服务端程序 srv.c (需要交叉编译,具体详见下面Makefile)

/*并发服务器可连接多个客户端通信fd=4  fd=5  fd=64:xxxx      send(4,"xxxx")5:yyyy 		send(5,"yyyy")6:zzzz		send(6,"zzzz")*/#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include void led_op(int led, int on)
{int fd;fd = open("/dev/led", O_RDWR);if(fd < 0) {perror("open");exit(1);}write(fd, &on, sizeof(on));close(fd);
}//线程
void *send_mess(void *arg)
{char buf[50] ;char *p;int newfd;int ret;while (1) {bzero(buf, 50);printf("pls input a string:");fgets(buf, 50, stdin); // 5:hellop = strtok(buf, ":");// p->"5"if(p == NULL) {printf("input string format err\n");continue;}newfd = atoi(p); // "5"->5p = strtok(NULL, ":");if(p == NULL) {printf("input string format err\n");continue;}ret = send(newfd, p, strlen(p), 0);printf("send=%d\n", ret);///if(strncmp(p, "quit", 4) == 0) break;}return NULL;}//子线程
void * recv_mess(void *arg)
{//收发客户端数据int ret;int newfd = *((int *)arg);char buf[50];char *p;int on=-1;int led=0;while (1) {bzero(buf, 50);ret = recv(newfd, buf, sizeof(buf), 0);printf("recv mess:%s",buf);//printf("ret=%d\n", ret);p=strtok(buf," ");// 将led on分隔为 led  第1次分隔用bufif (p != NULL) {printf("%s\n",p);if (strncmp(p, "led", 3) == 0)led=1;}p=strtok(NULL," "); //if (p != NULL) {			printf("%s\n",p);if (strncmp(p, "on", 2) == 0)on=1;if (strncmp(p, "off", 3) == 0)on=0;}if (led==1 && on == 1) led_op(led, on);if (led==1 && on == 0)led_op(led, on);if(strncmp(buf, "quit", 4) == 0) break;if (ret == 0) {printf("client offline\n");break; //客户端下线}}		return NULL;
}int main(void)
{int fd;// 服务端用来建立fd连接int newfd; //服务端和客户端成功建立连接后用来通信的newfdint ret;//1.买电话fd = socket(AF_INET, SOCK_STREAM, 0);if (fd == -1) {perror("socket");exit(1);}int on=1;setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); //重用端口号 on设置为真//2.绑卡struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(12346);server.sin_addr.s_addr = inet_addr("192.168.10.228");ret = bind(fd, (struct sockaddr *)&server, sizeof(server));if (ret == -1) {perror("bind");exit(1);}//3.监听listen(fd, 5);//4.接听struct sockaddr_in client;socklen_t len = sizeof(client);printf("tcp server start...\n");system("netstat -an | grep 12346");	//查看tcp连接状态//创建线程pthread_t tid;pthread_create(&tid, NULL, send_mess, NULL); //服务端键盘输入字符串发送给客户端while (1) {newfd = accept(fd, (struct sockaddr *)&client, &len); //没有连接就一直阻塞if (newfd == -1) {perror("accept");exit(1);}printf("ip=%s\n", inet_ntoa(client.sin_addr));printf("port=%d\n", ntohs(client.sin_port));printf("客户端 fd=%d 上线!\n", newfd);//创建线程,接收客户端数据pthread_create(&tid, NULL, recv_mess, &newfd);}//6.挂电话close(fd);return 0;}

开发板led 驱动程序 led_drv.c

/*
向内核动态申请主设备号
实现文件操作接口
动态创建设备文件创建设备类创建设备文件容错处理
面向对象的封装硬件初始化*/#include 
#include 
#include 
#include 
#include 
#include 
#include //采用面向对象方式来封装这些全局变量struct rk3399_led {int major ;struct class *cls;struct device *dev;unsigned int gpio_led2_green; //GPIO1_C7 55unsigned int gpio_led1; //GPIO0_B5 13int value; //保存用户空间传来的数据
}; static struct rk3399_led *led_device; //声明一个对象指针,没有创建对象static int led_open (struct inode *inode, struct file *filp)
{printk("call %s() @ %d\n ", __func__, __LINE__);return 0;}
static int led_close(struct inode *inode, struct file *filp)
{//设置引脚为输出功能低电平//gpio_direction_output(led_device->gpio_led2_green, 0);printk("call %s() @ %d\n ", __func__, __LINE__);return 0;
}ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *flags)
{int ret;ret = copy_from_user(&led_device->value, buf, size);if(ret) {printk(" copy_from_user fail\n");return -EFAULT;}printk(" value=%d\n", led_device->value);if (led_device->value) {//设置引脚为输出功能高电平gpio_direction_output(led_device->gpio_led2_green, 1);} else {//设置引脚为输出功能高电平gpio_direction_output(led_device->gpio_led2_green, 0);}printk("call %s() @ %d\n ", __func__, __LINE__);printk(" size=%ld\n", size);return size;
}static struct file_operations fops = {.owner = THIS_MODULE, //避免卸载模块.open = led_open,.release = led_close,	.write = led_write,
};//定义入口函数 添加模块时内核会调用
static int __init led_init(void)  //__init 优化  一次性
{int ret;led_device = kmalloc(sizeof(struct rk3399_led), GFP_KERNEL);if(led_device == NULL) {ret = -ENOMEM;return ret;}led_device->major = register_chrdev(0, "led_drv", &fops);if(led_device->major<0) {printk(" register_chrdev fail\n");ret= -EBUSY;goto err_register_chrdev;}printk(" major=%d\n", led_device->major);printk("call %s() @ %d\n ", __func__, __LINE__);//动态创建设备文件//创建设备类led_device->cls = class_create(THIS_MODULE, "led_cls");if (IS_ERR(led_device->cls)) {printk(" class_create fail\n");ret = PTR_ERR(led_device->cls);goto err_class_create;}//创建设备文件led_device->dev = device_create(led_device->cls, NULL, MKDEV(led_device->major, 0), NULL, "led"); // /dev/ledif (IS_ERR(led_device->dev)) {printk(" device_create fail\n");ret = PTR_ERR(led_device->dev);goto err_device_create;}//硬件初始化led_device->gpio_led2_green = 55;//申请引脚ret = gpio_request(led_device->gpio_led2_green, "led2_green");if(ret) {printk(" gpio_request fail\n");goto err_gpio_request;}//设置引脚为输出功能默认低电平gpio_direction_output(led_device->gpio_led2_green, 0);//释放引脚gpio_free(led_device->gpio_led2_green);led_device->gpio_led1 = 13;//申请引脚ret = gpio_request(led_device->gpio_led1, "led1");if(ret) {printk(" gpio_request fail\n");goto err_gpio_request;}//设置引脚为输出功能默认低电平gpio_direction_output(led_device->gpio_led1, 0);//释放引脚gpio_free(led_device->gpio_led1);return 0;
err_gpio_request:device_destroy(led_device->cls, MKDEV(led_device->major, 0));
err_device_create:class_destroy(led_device->cls);
err_class_create:unregister_chrdev(led_device->major, "led_drv");
err_register_chrdev:kfree(led_device);return ret;
}//定义出口函数 卸载模块时内核会调用
static void __exit led_exit(void) //_exit 优化 一次性
{device_destroy(led_device->cls, MKDEV(led_device->major, 0));class_destroy(led_device->cls);unregister_chrdev(led_device->major, "led_drv");kfree(led_device);printk("call %s() @ %d\n ", __FUNCTION__, __LINE__);
}module_init(led_init); //告诉内核我这个模块程序 入口(初始化)函数是led_init
module_exit(led_exit); //告诉内核我这个模块程序 入口(初始化)函数是led_exit
MODULE_AUTHOR("robin");
MODULE_DESCRIPTION("my first led world driver");
MODULE_LICENSE("GPL"); //开源许可协议

srv.c 和 led_drv 驱动程序Makefile

#指定内核源码根目录
KERN_DIR = /home/robin/work/kernel-rockchip-nanopi4-linux-v4.4.y
#KERN_DIR = /lib/modules/4.4.0-210-generic/build
#指定模块程序的目录
CUR_DIR = $(shell pwd)#指定应用
APP = srvall:make -C $(KERN_DIR) M=$(CUR_DIR) modulesaarch64-linux-gnu-gcc $(APP).c -o $(APP)
#	gcc $(APP).c -o $(APP)install:scp *.ko $(APP)  root@192.168.10.228:/drv_codeclean:make -C $(KERN_DIR) M=$(CUR_DIR) clean rm -f $(APP)# 指定当前目录下哪些文件作为内核模块程序来编译
obj-m = led_drv.o 

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...