[FFH]openharmony南向研究-Linux驱动框架+openharmony(3) 原创 精华
[FFH]openharmony南向研究-系统移植和驱动开发(3)Linux驱动框架+openharmony
前置
这次让我们来做一些用户态和内核态交互的事情
综述
如果是鸿蒙HarmonyOS设备内部嵌入式开发,则被称之为南向。如果是鸿蒙HarmonyOS应用开发,则被称之为北向。通俗可以说“南向指硬件方向开发,北向是指软件方向开发”。
南向是软硬件结合的嵌入式开发,一般用C、C++ 编程语言,注重硬件操作和能力封装,目标是提供北向API的内部实现。北向则是纯软件开发,一般用java、js、C++ 等编程语言,注重业务逻辑,目标是实现应用功能,满足客户需求。
举例一个实践场景:在一个室外噪音检测项目中,与用户交互的远程终端需要展示噪音数值,这时候开发显示数值的北向应用开发者仅需要调用一个api接口即可访问到api中回传的数值,那么api接口的具体实现则是在南向开发者的手中实现,需要做的工作及其流程为:设计硬件读取噪声信息的传感器板和传感器驱动电路电源电路等,在南向程序中实现HDF/linux原生驱动通过芯片读取传感器数值,在北向api的实现接口中调用这部分驱动,实现读取数值的工作。
显而易见的是在整个问题的解决中,南向的设备开发给与了设备与真实物理世界交互的可能性。尤其是在驱动开发这部分是极其重要的部分。
参考文献
南向程序开发流程
通用架构-内核部分
在openharmony官方手册中给出openharmony的其中一个特性是多端协同,于是在一些实现简单功能和需求低成本开发的设备中采用无北向设计,在传统意义上操作系统的用户态中也可以实现单独的南向应用程序,在命令行中启动一个状态机进程上电后即运行,通过该进程判断状态后不断读取各类所需传感器数据,通过一个启动软总线/wifi/任何物联网链路,回传到交互终端,这种流程中我们需要开发新的子系统新的驱动新的用户态应用程序,以完成我们的工程实现,
根据官方的分层和子系统图示可以看到,上层的北向应用相关子系统划分的较为清晰,在后续对于不同设备的支持和匹配上给出了良好的前瞻性。
在底层部分OpenHarmony针对不同量级的系统,分别使用了不同形态的内核,分别为LiteOS和Linux。在轻量系统、小型系统可以选用LiteOS;在小型系统和标准系统上可以选用Linux。
基于微内核设计,微内核仅包括了操作系统必要的功能模块(任务管理、内存分配等)处在核心地位具有最高权限,其他模块不具有最高权限,也就是说其他模块出现问题,对于整个系统的运行是没有阻碍的。微内核稳定性很高。且支持在编译时选择不同的内核或内核版本。
openharmony在我看来最为有趣的一点就是可以以多个子系统的方式共存很多种操作的可能性,想添加新的设备,加一个子系统,想实现一个新功能,添加一个子系统,当反过来的时候,想要单独只实现一个功能,在l0的系统板上,甚至可以砍掉所有其他的子系统和功能,如同在嵌入式单片机上运行的freertos那样只有一个调度功能的内核,其他什么都不做,这样减少了很多耦合的可能性和冗余的可能性。
还有一个真正在这部分中能看出华为野心和布局之大的部分,是内核抽象层和驱动子系统的可拓展性。原生的安卓内核虽然开发难度较低,基于linux的宏内核设计 ,宏内核包含了操作系统绝大多数的功能和模块,而且这些功能和模块都具有最高的权限,只要一个模块出错,整个系统就会崩溃,这也是安卓系统容易崩溃的原因。然而在鸿蒙系统中,甚至在一些尚未完全完成的子系统中还能查看到openharmony对于多种芯片多种芯片架构适配的思路,以及子系统解耦的工作进度。这让我觉得除了鸿蒙的生态不够,发展的时间不够,未来万物互联的时代必然有这款产品举足轻重的时候。
下面我们做一些具体的分析。下面是内核子系统的架构,多种内核共生共存,但是又可以随时把不要的部分砍掉,不会产生严重后果。
kernel/
├── linux
│ ├── linux-4.19 # OpenHarmony linux-4.19 Common kernel
│ ├── linux-5.10 # OpenHarmony linux-5.10 Common kernel
│ ├── build
│ │ ├── BUILD.gn # 编译框架GN文件
│ │ ├── kernel.mk # 内核编译文件
│ │ └── ohos.build # 内核编译组件文件
│ ├── patches
│ │ ├── linux-4.19 # linux-4.19 相关patch
│ │ │ └── hi3516dv300_patch
│ │ │ ├── hi3516dv300.patch # linux-4.19 hi3516dv300 SOC patch
│ │ │ └── hdf.patch # linux-4.19 hi3516dv300 hdf patch
│ │ └── linux-5.10
│ │ └── hi3516dv300_patch
│ │ ├── hi3516dv300.patch # linux-5.10 hi3516dv300 SOC patch
│ │ └── hdf.patch # linux-5.10 hi3516dv300 hdf patch
│ └── config
│ ├── linux-4.19
│ │ └── arch
│ │ └── arm
│ │ └── configs
│ │ ├── hi3516dv300_small_defconfig # 厂商Hisilicon对应的开源开发板Hi3516dv300小型系统的defconfig
│ │ ├── hi3516dv300_standard_defconfig # 厂商Hisilicon对应的开源开发板Hi3516dv300标准系统的defconfig
│ │ ├── small_common_defconfig # 小型系统的内核的common defconfig
│ │ └── standard_common_defconfig # 标准系统的内核的common defconfig
│ └── linux-5.10
│ └── arch
│ └── arm
│ └── configs
│ ├── hi3516dv300_small_defconfig # 厂商Hisilicon对应的开源开发板Hi3516dv300小型系统的defconfig
│ ├── hi3516dv300_standard_defconfig # 厂商Hisilicon对应的开源开发板Hi3516dv300标准系统的defconfig
│ ├── small_common_defconfig # 小型系统的内核的common defconfig
│ └── standard_common_defconfig # 标准系统的内核的common defconfig
└── liteos_a # liteos内核基线代码
├── apps # 用户态的init和shell应用程序
├── arch # 体系架构的目录,如arm等
│ └── arm # arm架构代码
├── bsd # freebsd相关的驱动和适配层模块代码引入,例如USB等
├── compat # 内核接口兼容性目录
│ └── posix # posix相关接口
├── drivers # 内核驱动
│ └── char # 字符设备
│ ├── mem # 访问物理IO设备驱动
│ ├── quickstart # 系统快速启动接口目录
│ ├── random # 随机数设备驱动
│ └── video # framebuffer驱动框架
├── fs # 文件系统模块,主要来源于NuttX开源项目
│ ├── fat # fat文件系统
│ ├── jffs2 # jffs2文件系统
│ ├── include # 对外暴露头文件存放目录
│ ├── nfs # nfs文件系统
│ ├── proc # proc文件系统
│ ├── ramfs # ramfs文件系统
│ └── vfs # vfs层
├── kernel # 进程、内存、IPC等模块
│ ├── base # 基础内核,包括调度、内存等模块
│ ├── common # 内核通用组件
│ ├── extended # 扩展内核,包括动态加载、vdso、liteipc等模块
│ ├── include # 对外暴露头文件存放目录
│ └── user # 加载init进程
├── lib # 内核的lib库
├── net # 网络模块,主要来源于lwip开源项目
├── platform # 支持不同的芯片平台代码,如Hi3516DV300等
│ ├── hw # 时钟与中断相关逻辑代码
│ ├── include # 对外暴露头文件存放目录
│ └── uart # 串口相关逻辑代码
├── platform # 支持不同的芯片平台代码,如Hi3516DV300等
├── security # 安全特性相关的代码,包括进程权限管理和虚拟id映射管理
├── syscall # 系统调用
└── tools # 构建工具及相关配置和代码
(本篇主要将linux内核原生驱动部分,同时也先分析linux内核有关的部分)
在linux内核部分中有两个内核子系统的子系统分别是:kernel_linux_patches,kernel_linux_config分别起着不同的作用。
patches:内核的Patch组成模块,在编译构建流程中,针对具体芯片平台,合入对应的架构驱动代码,进行编译对应的内核镜像
config:
通用配置文件
针对不同的内核版本,config将给出不同内核版本的对应不同的系统的参考通用配置文件,如下:
针对标准系统给出对应的参考通用配置文件:standard_common_defconfig;
针对小型系统给出对应的参考通用配置文件:small_common_defconfig。
在厂商或者自己设计了openharmony的开发板后进行测试和原生驱动开发时需要参考官方文档对这两部分进行有关配置,在进行一些驱动开发和测试过程中也需要对着几部分进行一些参考。
//TODO: 内核这部分还有很多东西没有仔细分析和讲解,后续会补全这一节的v2.0
通用架构–驱动部分
首先先要提出的是在openharmony标准系统中支持两种驱动开启的方式,分别是linux原生驱动和openharmony支持的HDF框架,因为linux原生驱动需要处理一些额外的事情,从下图(来自HDF驱动框架探路(二):openharmony最新源码,打通应用态到内核态-开源基础软件社区-51CTO.COM)就可以看出linux驱动需要几步复杂操作才能完成的事情,在HDF框架中仅需要简单的几步即可完成。
但是为了更深入理解驱动架构和使用方法,应当先从linux架构的角度入手,而且现成的方案会更多,对架构进行深入的学习和研究。再来使用HDF框架就会轻车熟路十分轻松了。
驱动架构要经过操作系统的硬件抽象层
linux内核驱动实现一个点灯流程
附件中有原理图
找到原理图,一个是按键一个是GPIO输出,查看管脚和系统管理器的设定方法
zh-cn/device-dev/driver/driver-platform-gpio-des.md · OpenHarmony/docs - Gitee.com(系统驱动管理器的配置文档)
LED的GPIO为 GPIO2_3
根据文档中的公式算出
按钮GPIO编号: 1
灯的GPIO编号: 19
处理按钮时使用的是kernel polling来监控系统文件的状态变化,进而进行一些有效的中断响应
代码文件led.cpp
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<poll.h>
int gpio_export(int pin);
int gpio_unexport(int pin);
int gpio_direction(int pin, int dir);
int gpio_write(int pin, int value);
int gpio_read(int pin);
int gpio_edge(int pin, int edge);
int gpio_export(int pin)
{
char buffer[64];
int len;
int fd;
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd < 0)
{
printf("Failed to open export for writing!\n");
return(-1);
}
len = snprintf(buffer, sizeof(buffer), "%d", pin);
printf("%s,%d,%d\n",buffer,sizeof(buffer),len);
if (write(fd, buffer, len) < 0)
{
printf("Failed to export gpio!");
return -1;
}
close(fd);
return 0;
}
int gpio_unexport(int pin)
{
char buffer[64];
int len;
int fd;
fd = open("/sys/class/gpio/unexport", O_WRONLY);
if (fd < 0)
{
printf("Failed to open unexport for writing!\n");
return -1;
}
len = snprintf(buffer, sizeof(buffer), "%d", pin);
if (write(fd, buffer, len) < 0)
{
printf("Failed to unexport gpio!");
return -1;
}
close(fd);
return 0;
}
int gpio_direction(int pin, int dir)
{
static const char dir_str[] = "in\0out";
char path[64];
int fd;
snprintf(path, sizeof(path),"/sys/class/gpio/gpio%d/direction", pin);
fd = open(path, O_WRONLY);
if (fd < 0)
{
printf("Failed to open gpio direction for writing!\n");
return -1;
}
if (write(fd, &dir_str[dir == 0 ? 0 : 3], dir == 0 ? 2 : 3) < 0)
{
printf("Failed to set direction!\n");
return -1;
}
close(fd);
return 0;
}
//value: 0-->LOW, 1-->HIGH
int gpio_write(int pin, int value)
{
static const char values_str[] = "01";
char path[64];
int fd;
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin);
fd = open(path, O_WRONLY);
if (fd < 0)
{
printf("Failed to open gpio value for writing!\n");
return -1;
}
if (write(fd, &values_str[value == 0 ? 0 : 1], 1) < 0)
{
printf("Failed to write value!\n");
return -1;
}
close(fd);
return 0;
}
int gpio_read(int pin)
{
char path[64];
char value_str[3];
int fd;
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin);
fd = open(path, O_RDONLY);
if (fd < 0)
{
printf("Failed to open gpio value for reading!\n");
return -1;
}
if (read(fd, value_str, 3) < 0)
{
printf("Failed to read value!\n");
return -1;
}
close(fd);
return (atoi(value_str));
}
int gpio_edge(int pin, int edge)
{
const char dir_str[] = "none\0rising\0falling\0both";
int ptr;
char path[64];
int fd;
switch(edge)
{
case 0:
ptr = 0;
break;
case 1:
ptr = 5;
break;
case 2:
ptr = 12;
break;
case 3:
ptr = 20;
break;
default:
ptr = 0;
}
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/edge", pin);
fd = open(path, O_WRONLY);
if (fd < 0)
{
printf("Failed to open gpio edge for writing!\n");
return -1;
}
close(fd);
return 0;
}
int main()
{
int gpio_fd, ret;
struct pollfd fds[1];
char buff[10];
gpio_unexport(41);
gpio_unexport(1);
gpio_export(41);
gpio_direction(41, 1);//output out
gpio_write(41, 1);
//1按钮初始化
gpio_export(19);
gpio_direction(1, 0);//input in
gpio_edge(1,2);
gpio_fd = open("/sys/class/gpio/gpio1/value",O_RDONLY);
fds[0].fd = gpio_fd;
fds[0].events = POLLPRI;
while(1)
{
ret = poll(fds,1,8125);
if( ret == -1 )
printf("poll\n");
if( fds[0].revents & POLLPRI)
{
ret = lseek(gpio_fd,0,SEEK_SET);
if( ret == -1 )
printf("lseek\n");
ret = read(gpio_fd,buff,10);//读取按钮值,但这里没使用
if( ret == -1 )
printf("read\n");
int status = gpio_read(41);
printf("41 = %d\n",status);
gpio_write(41, 1 - status);
}
printf("one loop\n");
//usleep(5);
}
return 0;
}
子系统构建
|-- Build.gn
|–led.cpp
|–ohos.build
其他操作流程
import("//build/ohos.gni")
import("//drivers/adapter/uhdf2/uhdf.gni")
ohos_executable("led") {
sources = [
"led.c"
]
subsystem_name = "led"
part_name = "led_part"
}
ohos.build
{
"subsystem": "led",
"parts": {
"led_part": {
"module_list": [
"//led:led"
],
"test_list": [ ]
}
}
}
注册子系统到主系统
找到harmonyos\build\subsystem_config.json
添加
"demo": {
"project": "hmf/led",
"path": "led",
"name": "led",
"dir": ""
}
注册到产品列表
找到harmonyos\productdefine\common\products\Hi3516DV300.json
"led:led_part": {},
配置完成运行编译程序
./build.sh --product-name Hi3516DV300
即可实现点灯效果
对linux内核开发驱动的普遍适应流程解析
参考文献:
其实linux内核驱动的写法和调用都不是非常困难的问题,都已经被抽象成了读写文件的流程,在这个过程中省略了操作寄存器等一系列复杂的流程。
在举出一些简单的例子(未经测试),开一个硬件iic
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#define Address 0x50
#define I2C_RETRIES 0x0701
#define I2C_TIMEOUT 0x0702
#define I2C_SLAVE 0x0703
#define I2C_BUS_MODE 0x0780
typedef unsigned char uint8;
uint8 rbuf[8] = {0x00}; //读出缓存
uint8 wbuf[8] = {0x01,0x05,0x06,0x04,0x01,0x01,0x03,0x0d}; //写入缓存
int fd = -1;
//函数声明
uint8 AT_Init(void);
uint8 i2c_write(int fd, uint8 reg, uint8 val);
uint8 i2c_read(int fd, uint8 reg, uint8 *val);
uint8 printarray(uint8 Array[], uint8 Num);
//AT初始化
uint8 AT_Init(void)
{
fd = open("/dev/i2c/0", O_RDWR); //允许读写
if(fd < 0)
{
perror("Can't open /dev/nrf24l01 \n"); //打开iic设备文件失败
exit(1);
}
printf("open /dev/i2c/0 success !\n"); //打开iic设备文件成功
if(ioctl(fd, I2C_SLAVE, Address)<0) { //设置iic从器件地址
printf("fail to set i2c device slave address!\n");
close(fd);
return -1;
}
printf("set slave address to 0x%x success!\n", Address);
if(ioctl(fd, I2C_BUS_MODE, 1)<0) //设置iic总线模式
printf("set bus mode fail!\n");
else
printf("set bus mode ok!\n");
return(1);
}
/*
uint8 AT_Write(uint8 *nData, uint8 Reg, uint8 Num)
{
write(fd, &Reg, 1); //
usleep(100); //延时100us
write(fd, nData, Num);
usleep(1000*4); //延时 4ms
return(1);
}
uint8 AT_Read(uint8 nData[], uint8 Reg, uint8 Num)
{
write(fd, &Reg, 1);
usleep(100); //延时100us
read(fd, nData, Num);
usleep(1000*4); //延时 4ms
return(1);
}
*/
static uint8 i2c_write(int fd, uint8 reg, uint8 val)
{
int retries;
uint8 data[2];
data[0] = reg;
data[1] = val;
for(retries=5; retries; retries--) {
if(write(fd, data, 2)==2)
return 0;
usleep(1000*10);
}
return -1;
}
static uint8 i2c_read(int fd, uint8 reg, uint8 *val)
{
int retries;
for(retries=5; retries; retries--)
if(write(fd, ®, 1)==1)
if(read(fd, val, 1)==1)
return 0;
return -1;
}
static uint8 printarray(uint8 Array[], uint8 Num)
{
uint8 i;
for(i=0;i<Num;i++)
{
printf("Data [%d] is %d \n", i ,Array[i]);
}
return(1);
}
int main(int argc, char *argv[])
{
int i;
AT_Init();
usleep(1000*100);
for(i=0; i<sizeof(rbuf); i++)
if(i2c_read(fd, i, &rbuf[i]))
break;
printarray(rbuf ,8);
printf("Before Write Data \n");
sleep(1);
for(i=0; i<sizeof(rbuf); i++)
if(i2c_write(fd, i, wbuf[i]))
break;
printarray(wbuf ,8);
printf("Writing Data \n");
sleep(1);
for(i=0; i<sizeof(rbuf); i++)
if(i2c_read(fd, i, &rbuf[i]))
break;
printarray(rbuf ,8);
printf("After Write Data \n");
close(fd);
}
甚至你在读完之后发现和单片机的开发没什么两样,甚至更简单了,那就ok啦,这就是操作系统带给开发者的超能力。
读写文件的目录一般就可以在/sys/class目录下找到,如果想单独开发一些硬件,目前没有用到,因为觉得linux原生驱动非常够用,项目实践过程中并不是很缺其他的一些东西。
总结
之后还会研究HDF开发,以及一些线程操作之类的事情,一个完整的系统还能做更多的事情,尤其是鸿蒙的新特性带给开发者的超能力还有很多,一点点探索。
学习楼主站在巨人肩膀上学习的方法。