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. 驱动基础
应用程序根据需求在驱动底层创建对应的驱动函数,驱动层创建好的接口函数与应用层IO函数一一对应,从面向对象的思想出发构造相应的 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 [email protected]:/drv_code
- u盘 sd卡 挂载
- 网络文件系统 nfs
- adb push
6. 代码实例
ubuntu 客户端程序 cli.c
/*
客户端循环收发一个字符串给服务端双向通信,服务端收发字符串, 注意在ubuntu运行只需gcc 编译即可
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
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 0
struct sockaddr_in client;
client.sin_family = AF_INET; //ipv4
client.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=6
4:xxxx send(4,"xxxx")
5:yyyy send(5,"yyyy")
6:zzzz send(6,"zzzz")
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
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:hello
p = strtok(buf, ":");// p->"5"
if(p == NULL) {
printf("input string format err\n");
continue;
}
newfd = atoi(p); // "5"->5
p = 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次分隔用buf
if (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; //服务端和客户端成功建立连接后用来通信的newfd
int 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 <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>
//采用面向对象方式来封装这些全局变量
struct rk3399_led {
int major ;
struct class *cls;
struct device *dev;
unsigned int gpio_led2_green; //GPIO1_C7 55
unsigned int gpio_led1; //GPIO0_B5 13
int 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("<kernel> copy_from_user fail\n");
return -EFAULT;
}
printk("<kernel> 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("<kernel> 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("<kernel> register_chrdev fail\n");
ret= -EBUSY;
goto err_register_chrdev;
}
printk("<kernel> 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("<kernel> 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/led
if (IS_ERR(led_device->dev)) {
printk("<kernel> 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("<kernel> 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("<kernel> 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 = srv
all:
make -C $(KERN_DIR) M=$(CUR_DIR) modules
aarch64-linux-gnu-gcc $(APP).c -o $(APP)
# gcc $(APP).c -o $(APP)
install:
scp *.ko $(APP) root@192.168.10.228:/drv_code
clean:
make -C $(KERN_DIR) M=$(CUR_DIR) clean
rm -f $(APP)
# 指定当前目录下哪些文件作为内核模块程序来编译
obj-m = led_drv.o