太累了,有空再写吧。
2024-02-27
三星投递简历
2024-02-27
OPPO投递简历
2024-03-01
VIVO投递简历
2024-03-01 11:15
VIVO测评
2024-03-01
米哈游投递简历
2024-03-04
OPPO重新投递简历
2024-03-04
华为投递简历
2024-03-05
字节投递简历
2024-03-05
小米投递简历
2024-03-09 08:00
OPPO笔试
2024-03-11 15:00
小米技术面试一、技术面试二
2024-03-12 10:00
小米技术面试三
2024-03-13 15:00
VIVO笔试
2024-03-13 19:00
华为笔试
2024-03-14 15:10
OPPO 技术面试
2024-03-16 14:50
OPPO 综合面试
2024-03-17 10:00
米哈游笔试
2024-03-19 21:30
新凯来个性测评
2024-03-20 10:00
OPPO HR面试
2024.03.20 19:00
虾皮笔试
2024-03-21 10:30
新凯来技术面试
2024-03-22 15:00
优博讯笔试
2024-03-23 15:00
新凯来综合面试
2024-03-24 10:00
新凯来电话资面
2024-03-26 11:00
OPPO录用意向沟通会
2024-03-26 16:00
VIVO线下面试(技术面+HR面)
2024-03-27 15:00
米哈游一面
2024-03-28 15:00
字节一面(已申请取消)
2024-04-01 15:00
米哈游二面
1 | SRCN-Android Platform开发 工程师 (江苏省·南京市) |
1 | 安卓应用工程师 |
VIVO的邮件会被Gmail视为垃圾邮件,难绷
1 | 移动客户端开发(Android) |
1 | 软件开发工程师 - 应用软件开发工程师 |
1 | 客户端开发工程师-Flow | 杭州 | 正式 | 研发 - 客户端 | 2024校招全职补录 | 职位 ID:A238115 |
1 | Android开发工程师(可接受Java方向)-澎湃OS |
1 | (24届春招)Android开发工程师 |
1 | 软件开发工程师(成都) |
1 | 校园招聘-Android系统开发工程师 |
前面我们学习了很多单片机的知识,接下来我们即将进入到ARM的世界~
本篇博文将从零开始实现一块基于STM32F103VBT6芯片的开发板上的LED灯的闪烁。
如何使用它下载到STM32板上呢?
首先将开发板上的COM口接入PC,在软件中搜索串口
并且选择CH 340
串口,波特率选择115200
即可。
然后选择HEX文件并按照如下选项进行勾选
准备就绪后,点击开始编程按钮,在开发板上按顺序快速按下REST和ISP按键即可下载。
该程序安装完成后记得安装本开发板的支持包Keil.STM32F1xx_DFP.xxx
(双击即可安装)
可在此处下载并安装最新的J-Link
可在此处下载并安装最新的STM32CubeMX(需要注册并登录)
安装完成后首次打开该应用,首先打开Embedded Software Packages Manager
选择STM32F1并下载最新的软件包
打开STM32CubeMX,选择ACCESS TO MCU SELECTOR
搜素STM32F103VBT6
,双击右侧对应的STM32F103VBT6
。
从原理图可以看出,LED灯的A - E分别对应应引脚PE8 - PE15,而LED_SEL对应PB3。
因此我们将其设置为推挽,即Output Push Pull,当然还可以为其添加标签,更便于标识。
将这9个引脚设置完毕后,我们接下来设置其调试模式。
进入System Core - SYS,在Debug处选择Serial Wire,防止开发板被上锁导致只能下载一次的问题。
这里我们使用时间中断来闪烁LED灯,因此我们需要配置时钟。
进入Clock Configuration,观察到默认频率为8MHz(实际上可以根据自己的需求进行调整,或使用外部时钟等等,这里我们使用系统内部的时钟即可)
返回Pinout & Configuration界面,进入Timers - TIM1
首先设置时钟源(Clock Source)为系统时钟(Internal Clock),然后在Parameter Settings中设置分频系数(Prescaler)、计数周期(Counter Period)以及自动重载(auto-reload preload)
假设分频系数(Prescaler)为A,计数周期(Counter Period)为B,时钟频率为C,则这里时间溢出公式为
我们将A设置为8000-1,B设置为1000-1,C为8Mhz,因此T = 1s,即我们的时间回调函数调用周期为1s。
接下来我们开启时间中断
在Project Manager中设置工程名称、路径、工具链。
在左侧选择Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheral
,即针对每个外设生成独立的.c和.h文件。
配置完成后,点击右上角GENERATE CODE按钮即可生成模板代码。
在工程目录下,LED.ioc
即为STM32CubeMX的工程文件,而MDK-ARM
下的LED.uvprojx
即为Keil工程文件。
使用Keil或其它工具打开该工程,我们可以发现STM32CubeMX已为我们生成了规范的目录结构。
在gpio.c
中我们可以找到IO口初始化的相关代码
1 | void MX_GPIO_Init(void) |
因此我们可以依葫芦画瓢在main.c
中为LED_SEL口使能,让其LED灯正常工作。
1 | int main(void) |
其中GPIO_PIN_SET
实为1,即高电平;同理GPIO_PIN_RESET
为0,即低电平。
在tim.c
中我们可以发现它有一个全局实例,时钟通过该实例进行区分。
欲开启该时钟,我们需要将其初始化。
1 | int main(void) |
然后编写时间中断函数即可。
1 | void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) |
这里我选择将LED灯A每1s进行一次翻转,从而实现闪烁效果。
代码编写好后,如果正确安装了J-Link驱动,将开发板上的SWD口接入PC,则可在设备管理器中找到如下条目
然后打开Keil,如下图配置J-Link调试。
进入J-Link设置,按如下配置端口和频率
然后编译
至此,我们将其下载到开发板上,按下REST键即可验证。
设置断点后,即可进入调试模式。
]]>IOS/MacOS对于模糊的理解已然是登峰造极,但我们平常使用普通的高斯模糊却做不出这种效果,这是为什么呢?如何用Photoshop做出类似于IOS/MacOS那样好看的模糊呢?
选中拷贝的图层,选择滤镜 - 模糊 - 高斯模糊,半径根据需要可调30-50,或者其他适合的半径
以一个圆角矩形为例,我们只需要其形状,因此该矩形的颜色或描边不需要管它。
然后右键矩形图层 - 栅格化图层,并用魔棒工具选中该矩形形状
然后选中模糊的图层,点击下方的添加图层蒙版按钮
删除矩形图层,即可得到模糊遮罩
但是效果看起来不太行!我们解锁模糊图层和蒙版,选中模糊图层,使用快捷键(Ctrl + u)打开色相窗口,将饱和度和明度进行适当调整。
这下看起来好很多了,我们选中遮罩层,调整一下遮罩的位置和大小。
针布戳
]]>最近有个项目需要一块专用单片机用于实现PWM调频、电量测量(实际上是电压测量)、光照测量、485以及串口通信,并且项目代码基于学校老师封装的BSP,由于其是为基于IAP15F2K61S2芯片设计的开发板,因此打算基于同款芯片重新设计一块。
工欲善其事,必先利其器。
原理图是设计流程中的基础,它是最终单片机上的一个个模块电路。由于是基于学校的开发板,因此可以借鉴其部分原理图。
打开嘉立创EDA,新建好工程后创建第一张原理图,然后打开元件库
搜索IAP15F2K61S2 LQFP44
在添加元件时,最好选择立创商城的元件,这样在购买的时候比较方便。
将其放置在原理图上,根据实际需求修改引脚:
VCC
引脚和GND
引脚之间连接了一个104
的电容,而104
指的是右上角是FM收音机模块的一部分,而这里不需要用到它。
添加好元件后就可以绘制原理图了,这里把接口换成了Type-C(使用倒是方便,但非常难焊)。
下图存在部分错误,会在后文中指出。
由于可以通过芯片在软件层实现PCA调频,因此可以借助MOS管导通特性,从而将输入的12V进行调频。
将P1.0/P1.1口接入NMOS管的栅极,12V外部输入接入PMOS管的源极,当P1.0/P1.1输入高电平时NMOS管源极和漏极导通,PMOS管栅极相当于接地,其源极和漏极导通,从而使12V输出到漏极,通过对导通时间与周期的比例(即占空比)的控制,即可控制输出电压。
其中二极管接地的作用是稳压和保护,防止反向击穿;电感通直流,阻交流;电容接地则是滤波。
V_Voltage0/V_Voltage1得到的值是2k电阻的电压,因此将值除以2乘以20即为所测电压。
这里没有直接接入光敏电阻,而是改成用连接器外接。
PCB设计是设计流程中的重要环节,它决定了最终成品板的模样。
将所有原理图绘制完成后,可以打开左侧工具栏中的设计管理器,检查是否还有未连接网络。
检查完成之后,单击设计 - 原理图转PCB或者快捷工具栏对应的图标,将原理图转为PCB。
后续如果原理图有更改,则可以选择更新PCB。
首先根据实际情况填写参数,新建PCB。
新建好后,就可以对PCB进行设计了。
每个人的设计风格和理念都不同,但仍需要遵守一定的准则,以保证开发板的稳定性。
根据原理图进行大致布局 → 微调元件位置 → 根据需要可选择放置焊盘 → 连线 → 微调元件位号位置,适当添加引脚标识(如GND等) → 添加泪滴(增强连线稳定性) → 铺铜(增强散热、屏蔽、保证PCB工艺电镀效果等)
绘制完成后,可以查看成品的各种样图。
]]>跨端应用开发一直都是热门需求,目前已经有很多方案可以实现跨端开发,然而 Flutter 对于目前热门的高刷屏适配摆烂, JetPack Compose 仍在发育之中, Electron 优势在于前端技术栈却亦受限于此,但如果我们熟悉前端开发,Electron仍是一个不错的选择。
本文以 Vue2 热门框架 Quasar 为例,记录其Windows桌面程序搭建流程。
NVM 对于Node版本管理非常方便,请参考其安装文档安装 NodeJS 。
相比于NPM,我更青睐 Yarn 。
安装
1 | npm install --global yarn |
验证
1 | yarn --version |
Quasar脚手架可以方便地为我们创建Quasar工程。
1 | yarn global add @quasar/cli |
1 | yarn create quasar |
根据提示选择相关配置来创建Quasar工程。
1 | yarn config set electron_mirror https://cdn.npm.taobao.org/dist/electron/ |
先进入工程目录,再添加Electron模式。
1 | quasar mode add electron |
1 | quasar dev -m electron |
PyTorch是开源的深度学习框架,目的是加速从研究原型到产品开发的过程。其SDK主要基于Python。而其模型训练支持CPU与GPU、支持分布式训练、云部署,针对深度学习特定领域有不同的丰富的扩展库。
深度学习项目的开发大致可以分为两个阶段:
相比于PyTorch、TensorFlow等为开发者所熟知的训练框架,推理部署的框架却显得有些默默无闻,但是它在深度学习模型落地过程中发挥着不可替代的作用。正是在这样的背景之下,英特尔在2018年发布了专门针对CPU、iGPU(集成显卡)、FPGA、ARM等硬件单元加速的模型部署与加速推理框架OpenVINO™。
OpenVINO™是英特尔发布的一套支持快速开发视觉、语音识别、自然语言处理应用的框架,受益于人工智能技术的快速发展,框架采用了最新的人工智能神经网络包括卷积神经网络、循环神经网络、注意力机制网络等模型。实现视觉与非视觉任务的底层硬件加速、达到最佳性能,支持人工智能应用从云端到边缘的部署与推理全链路技术。
建议使用 Conda 安装 PyTorch ,本篇笔记也将以Conda安装方式为例。笔者的环境是Windows11 + Anaconda + CUDA11.6,而Linux或Mac平台的环境配置大同小异,这里就不过多赘述。
以CUDA11+为例,首先需要安装CUDA驱动
安装完成后,检测安装版本
1 | nvidia-smi |
1 | conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch |
若没有NVIDIA系显卡或其不支持CUDA加速,则可选择安装仅CPU版本。
1 | conda install pytorch torchvision torchaudio cpuonly -c pytorch |
在终端进入Python3控制台(笔者python
命令默认链接到Python3)
1 | python |
导入PyTorch
1 | import torch |
测试torch.rand()
函数
1 | torch.rand(5, 3) |
若安装成功,则会出现类似于以下的输出
测试是否支持CUDA
1 | import torch |
很多人学习深度学习框架面临的第一个问题就是其专业术语跟基本的编程概念与传统面向对象编程不同,这是初学者面临的第一个学习障碍。
在主流的面向对象编程语言中,结构化代码最常见的关键字是if
、else
、while
、for
等关键字,而在深度学习框架中编程模式主要是基于计算图、张量数据、自动微分、优化器等组件构成。
面向对象编程运行的结果是交互式可视化的,而深度学习通过训练模型生成模型文件,然后再使用模型预测、本质数据流图的方式工作。所以学习深度学习框架首先必须理清深度学习编程中计算图、张量数据、自动微分、优化器这些基本术语概念,下面分别解释如下:
张量(Tensor)是深度学习框架中需要理解的最重要的一个概念,张量的本质是数据,在深度学习框架中一切的数据都可以看成张量。
深度学习中的计算图是以张量数据为输入,通过算子运算,实现对整个计算图参数的评估优化。但是到底什么是张量?可以看下面这张图:
上图中标量、向量、数组、3D、4D、5D数据矩阵在深度学习框架中都被称为张量。可见在深度学习框架中所有的数据都是张量形式存在,张量是深度学习数据组织与存在一种数据类型。
深度学习主要是针对张量的数据操作。这些数据操作从简单到复杂,多数都是以矩阵计算的形式存在。最常见的矩阵操作就是加减乘除,此外卷积、池化、激活也是模型构建中非常有用的算子/操作数。Pytorch支持自定义算子操作,可以通过自定义算子实现复杂的网络结构,构建一些特殊的网络模型。张量跟算子/操作数一起构成了计算图,它们是也是计算图的基本组成要素。
深度学习基于计算图完成模型构建,实现数据在各个计算图节点之间流动,最终输出。因此计算图又被称为数据流图。
根据构建计算图的方式不同还可以分为静态图与动态图。Pytorch默认是基于动态图的方式构建计算图。
动态图采用类似Python语法,可以随时运行,灵活修改调整。
而静态图则是效率优先,但是在图构建完成之前无法直接运行。
可以看出动态图更加趋向于开发者平时接触的面向对象的编程方式,也更容易被开发者理解与接受。
下图是一个简单的计算图示例:
图中最底层三个节点表示计算图的输入张量数据节点(a、b、c),剩下节点表示操作,带箭头的线段表示数据的流向。
使用Pytorch构建神经网络(计算图)模型之后,一般都是通过反向传播进行训练,反向传播算法使用损失函数功能对神经网络中每个参数根据梯度进行参数值的调整。
为了计算这些梯度完成参数调整,深度学习框架中都会自带一个叫做自动微分的内置模块,来自动计算神经网络模型训练时的各个参数梯度值并完成参数值更新,这种技术就是深度学习框架中的自动微分。
张量在PyTorch深度学习框架中表示数据,有几种不同的方式来创建与声明张量数据。
1 | import torch |
输出
1 | tensor([[2., 3.], |
其中torch.tensor()
默认的数据类型是flaot32,这点从a.dtype
的打印结果上也得了印证。
torch.tensor
函数支持从NumPy数组直接转换为张量数据。
1 | import numpy as np |
输出
1 | tensor([[1, 2], |
函数返回的数据类型将会根据NumPy数组自动识别。
PyTorch框架支持类似MATLAB的数组初始化方式,可以定义数组的维度,然后初始化为零。
1 | import torch |
输出
1 | tensor([[0., 0., 0., 0.], |
可使用torch.ones()
函数初始化为1
1 | import torch |
输出
1 | tensor([[1., 1., 1., 1.], |
在实际的开发中,经常需要随机初始化一些张量,可通过torch.rand()
等函数实现
1 | import torch |
输出
1 | v1: tensor([[0.6751, 0.0717, 0.4391], |
以上图为例,用代码将它实现出来:
1 | import torch |
输出
1 | y: tensor([[ 5.8000], |
可用如下代码进行常见的数据类型转换:
1 | import torch |
输出
1 | tensor([1., 2., 3., 4., 5., 6.]) torch.float32 |
可用如下代码进行常见的维度转换:
1 | import torch |
输出
1 | a: tensor([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.]) |
除此之外,还可以使用基于tensor
的维度转换函数tensor.view()
1 | import torch |
输出
1 | a: tensor([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.]) |
通道交换是PyTorch中处理张量数据常用操作之一。
1 | import torch |
输出
1 | x: tensor([[[-0.9932, -2.1915, -0.8266, -1.8298, 0.5624], |
寻找最大值是PyTorch中处理张量数据常用操作之一。
1 | import torch |
输出
1 | x: tensor([ 2., 3., 4., 12., 3., 5., 8., 1.]) |
PyTorch支持CPU与GPU计算,默认创建的tensor是CPU版本的,要想使用GPU版本,首先需要检测GPU支持,然后转换为GPU数据,或者直接创建为GPU版本数据
1 | import torch |
输出
1 | name: NVIDIA GeForce RTX 2060 |
线性回归的本质就是根据给出二维数据集来拟合生成一条直线,如下图:
左图是一组圆点表示的二维坐标点数据集,直线是根据线性回归算法生成的。
右图则是根据坐标点数据集生成的一个非线性回归例子。
现在我们已经可以很直观地了解什么是线性回归了,但线性回归是怎么找到这条直线的?
我们可以通过PyTorch构建一个简单的计算图来不断学习,最终得到一个足够逼近真实直线的参数方程,这个过程被称为线性回归的学习/训练过程。
最常见的直线方程如下:
假设有一组二维坐标点数据集:
一 | 二 | 三 | 四 | 五 | 六 | |
---|---|---|---|---|---|---|
x | 1 | 2 | 0.5 | 2.5 | 2.6 | 3.1 |
y | 3.7 | 4.6 | 1.65 | 5.68 | 5.98 | 6.95 |
随机赋值初始k、b两个参数,根据直线方程,通过x可以得到对应的
假设当前参数为A(k, b),新参数为B(k, b),我们可以通过下面的公式来更新k、b两个参数:
其中η称为学习率,grad(η)是对应的参数梯度,可根据深度学习框架的自动微分机制得到。这样就实现了线性回归模型的构建与训练过程,最终可根据输入的迭代次数运行并输出回归直线的两个参数,从而完成线性回归的求解。
PyTorch提供了丰富的函数,可以帮助我们快速搭建线性回归模型并完成训练预测。
1 | import numpy as np |
1 | # 继承torch.nn.Module |
1 | data_input_dim = 1 # 输入维度为1 |
1 | # 迭代训练 |
1 | predicted_y = model(torch.from_numpy(data_x).requires_grad_()).data.numpy() # 得到预测结果 |
最终完整代码为:
1 | import numpy as np |
最终得到:
OpenVINO™ ToolKit主要功能包含:
人工智能应用/模型的开发在第二阶段模型部署中,可以借助OpenVINO™ ToolKit的压缩量化、推理加速能力达到模型推理时的最佳性能(速度与精度)。相比第一阶段,第二阶段更为重要,它决定模型最终是否可以实现商业目标,带来商业价值。英特尔发布的开源版本OpenVINO™ ToolKit支持从云端到边缘的模型部署,通过自身人工智能技术优势着力解决人工智能落地环节的商业痛点。
上图中矩形框内的部分是OpenVINO™的核心功能。矩形框外表示第三方提供的模型与上层应用。
跨平台的命令行工具包,支持导入来自主流的深度学习框架的模型,模型文件可能由TensorFlow、PyTorch、Caffe、MXNet、ONNX等深度学习框架与工具生成。模型优化器支持对导入模型的转换、优化、导出。
一个统一的API接口层,支持对深度学习模型的高效推理,支持跨操作系统、多种底层硬件的异构模式推理计算,这些硬件包括Intel CPU、Intel集成显卡、NCS2计算棒、VPU、FPGA等。
一系列的示例代码文件,通过控制台运行演示如何在第三方应用中集成推理引擎开发。
一个基于WEB端的智能化图形交互界面,允许你更加灵活地尝试OpenVINO™提供各种组件功能。
一个验证与执行量化INT8精度的工具,此功能包含在模型优化器命令行文件夹下面。
OMZ主要有两个部分。
第一部分是Demos教程,主要包括Python与C++的SDK教程,教程主要涉及计算机视觉与语音识别相关的内容。
第二部分是模型,包含Intel提供与其它公开支持OpenVINO™部署推理的模型,总数超过240+,对于很多常见的视觉任务都无需再训练模型,直接使用模型再通过推理引擎部署即可。
需要注意的是public
(非Intel提供)部分的模型,需要开发者自己转换为IR格式中间文件,才能通过推理引擎部署调用。
OpenVINO™ ToolKit在支持深度学习模型部署的同时,还把OpenCV作为支持传统视觉算法处理的组件,集成到了OpenVINO™ ToolKit中,因而可以自动获取OpenCV框架的支持,这对熟悉OpenCV框架的开发者来说是一个大大的福利!并且OpenVINO™提供的官方教程中,图像预处理与推理引擎输出的后处理,图像与图形绘制等均采用了OpenCV相关函数完成,对大多数OpenCV开发者来说看这些代码毫无违和感。
打开Intel® Distribution of OpenVINO™ Toolkit
如上图,由于后续基本上采用Python开发,所以我们选择PIP的方式安装
选择PyTorch框架,得到如下安装代码:
1 | pip install openvino-dev[pytorch]==2022.1.0 |
未完待续…
OpenCV是计算机视觉领域一款热门的框架,人脸识别、二维码处理、深度学习中都能见到它的身影~
在官网中下载OpenCV源码,本文以OpenCV – 4.5.5为例。
安装相关依赖
1 | sudo apt-get install libgtk2.0-dev |
解压至任意目录
1 | unzip opencv-4.5.5.zip |
进入解压后的文件夹,创建build
文件夹并进入
1 | cd opencv-4.5.5 |
CMake生成Makefile
1 | cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr/local/ .. |
编译OpenCV(-j
为线程数,视实际情况更改,太大可能会爆内存,实在不行就单线程吧hhh)
1 | make -j8 |
安装
1 | sudo make install |
验证(运行源码根目录下samples/cpp/example_cmake
后弹出Hello OpenCV
即为成功)
1 | cd ../samples/cpp/example_cmake |
通过pip
安装
1 | pip3 install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple |
本文主要记录基于Python的OpenCV笔记
1 | import cv2 |
1 | img = cv2.imread("ari.jpg") |
第一个参数是图片的路径,绝对路径和相对路径均可。
第二个参数是可选参数,可填以下内容:
1 | cv2.imshow("ari", img) |
第一个参数是窗口的标题,第二个参数传入读取的img对象。
1 | cv2.namedWindow("ari") |
第一个参数是窗口的标题。
第二个参数是可选参数,可填以下内容:
1 | cv2.waitKey(0) |
当调用imshow()
后,需要再调用waitKey()控制窗口显示时间。
当参数≤0时,则窗口一直等待,返回 -1 或按键值,当为$time
时,则窗口显示$time
ms,在此期间按键则返回按键值,否则返回 -1 。
1 | cv2.imwrite("ari_out.jpg", img) |
第一个参数是保存的路径及名称,第二个参数传入读取的img对象。
1 | import cv2 |
最近有一个项目需要将单目摄像机拍摄到的照片中的像素坐标转换为世界坐标,于是研究了一下相关的原理和代码。
在相机模型中,我们需要理解四个坐标系以及它们之间的关系:
假设像素在u轴和v轴方向上的物理尺寸为dx和dy。
根据上图可以推导出该公式:
转换为矩阵形式:
从世界坐标系变换到相机坐标系属于刚体变换,物体不会发生形变,只需进行旋转和平移。
如上图,R表示旋转矩阵,T表示旋转向量。
用矩阵表示其关系:
令:
因此可表示为
如图,根据相似三角形原理,可得:
可变换为:
因此可用矩阵表示:
将上面的矩阵公式综合,即可得出以下公式:
其中,
称为相机内参。
称为相机外参。
我们的需求是根据像素坐标求世界坐标,而世界坐标系建立在地面,因此可令Zw=0。
即:
将相机外参展开:
化简可得:
将上式改写为AX=B的形式:
解出这个矩阵方程即可求解 世界坐标(Xw, Yw, 0) 以及 相机坐标系原点到所求点的直线距离Zc !
未完待续…
BlogOS是Philipp Oppermann用Rust语言编写的面向x86架构的简单操作系统。
《ARM v8之旅》将作为 湖南大学2022年操作系统课程实验 个人参考笔记。
更详细的解析请参考 rust写个操作系统:课程实验blogos移至armV8深度解析 。
参考代码:下载
本文以Windows Subsystem for Linux 2为环境,可参考 Windows Subsystem for Linux 2 的艺术 搭建。
输入以下命令
1 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
若网络正常,则会出现以下输出,键入1
,执行默认安装。
安装完成后,激活Rust环境
1 | source $HOME/.cargo/env |
查看版本
1 | rustc -V |
根据文档,实验需要用到Nightly版本
1 | rustup default nightly |
安装GCC
1 | sudo apt-get install gcc |
安装相关工具
1 | cargo install cargo-binutils rustfilt |
若安装GCC后仍无法正常
cargo install cargo-binutils rustfilt
,请尝试将软件源更换为阿里源(参见 Windows Subsystem for Linux 2 的艺术 ),再重新安装一次GCC。
键入以下命令
1 | rustup target add aarch64-unknown-none-softfloat |
键入以下命令
1 | sudo apt-get install qemu qemu-system-arm |
安装必要环境
1 | sudo apt-get install libncursesw5 libpython2.7 axel |
创建交叉编译工具链目录
1 | mkdir ToolChain && cd ToolChain |
使用axel多线程下载工具链 AArch64 ELF bare-metal target (aarch64-none-elf)
1 | axel -n 32 -a https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz |
解压
1 | tar -xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz |
由于我们的目标是编写一个操作系统,所以我们需要创建一个独立于操作系统的可执行程序,又称独立式可执行程序(Freestanding Executable)或裸机程序(Bare-metal Executable)。
这意味着所有依赖于操作系统的库我们都不能使用。比如
std
中的大部分内容(io
,thread
,file system
, etc…)都需要操作系统的支持,所以这部分内容我们不能使用。但是,不依赖于操作系统的Rust的语言特性我们还是可以继续使用的,比如:迭代器、模式匹配、字符串格式化、所有权系统等。这使得Rust依旧可以作为一个功能强大的高级语言,帮助我们编写操作系统。
回到Home
目录
1 | cd ~ |
新建名为rui_armv8_os
的项目
1 | cargo new rui_armv8_os --bin --edition 2021 |
进入rui_armv8_os
目录
1 | cd rui_armv8_os |
创建实验所需文件
1 | touch src/panic.rs src/panic.rs src/start.s aarch64-qemu.ld aarch64-unknown-none-softfloat.json |
创建.cargo
文件夹
1 | mkdir .cargo |
创建.cargo/config.toml
1 | touch .cargo/config.toml |
使用VSCode打开
1 | code . |
VSCode安装Rust、Rust-Analyzer插件
编辑src/main.rs
1 | #![no_std] // 不使用标准库 |
编辑src/panic.rs
1 | use core::panic::PanicInfo; |
编辑src/start.s
1 | .globl _start |
编辑aarch64-qemu.ld
1 | ENTRY(_start) |
编辑aarch64-unknown-none-softfloat.json
1 | { |
编辑.cargo/config.toml
1 | [unstable] |
编辑Cargo.toml
1 | [package] |
在项目根目录下执行
1 | cargo build |
运行
1 | qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
QEMU进入调试,启动调试服务器,默认端口1234
关闭之前运行的终端,打开一个新的终端,进入rui_armv8_os
目录
1 | cd rui_armv8_os |
启动调试
1 | qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -S -s |
重新打开一个终端,进入工具链bin
目录
1 | cd ~/ToolChain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin |
导出工具链路径临时变量
1 | export ToolChainPath=`pwd` |
进入rui_armv8_os
目录
1 | cd ~/rui_armv8_os |
配置临时工具链环境(这里的$ToolChainPath
即是刚刚导出的临时变量)
1 | export PATH=$ToolChainPath:$PATH |
启用GDB调试客户端
1 | aarch64-none-elf-gdb target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
设置调试参数,开始调试
连接调试客户端
1 | target remote localhost:1234 |
查看汇编码
1 | disassemble |
单步运行
1 | n |
参考代码:下载
操作系统介于硬件和应用程序之间,向下管理硬件资源,向上提供应用编程接口。设计并实现操作系统需要熟悉底层硬件的组成及其操作方法。
本系列实验都会在QEMU模拟器上完成,首先来了解一下模拟的机器信息。可以通过下列两种方法:
查看QEMU关于virt的描述, 或者查看QEMU的源码,如GitHub上的virt.h和virt.c。
virt.c
中可见如下有关内存映射的内容。
1 | /* Addresses and sizes of our components. |
首先安装DTC
1 | sudo apt-get install device-tree-compiler |
新建一个设备树目录并进入
1 | mkdir ~/device && cd ~/device |
导出DT
1 | qemu-system-aarch64 -machine virt,dumpdtb=virt.dtb -cpu cortex-a53 -nographic |
-machine virt
指明机器类型为virt,这是QEMU仿真的虚拟机器。
用DTC将导出的Device Tree Blob转换为Device Tree Source
1 | dtc -I dtb -O dts -o virt.dts virt.dtb |
用文本编辑器打开virt.dts
,可以发现如下内容
1 | pl011@9000000 { |
由上可以看出,virt机器包含有pl011的设备,该设备的寄存器在
0x9000000
开始处。pl011实际上是一个UART设备,即串口。可以看到virt选择使用pl011作为标准输出,这是因为与PC不同,大部分嵌入式系统默认情况下并不包含VGA设备。
我们参照Writing an OS in Rust - VGA Text Mode (使用Rust编写操作系统(三):VGA字符模式)来实现
println!
宏,但与之不同的是,我们使用串口来输出,而不是通过操作VGA的Frame Buffer。
进入rui_armv8_os
目录
1 | cd ~/rui_armv8_os |
新建src/uart_console.rs
1 | touch src/uart_console.rs |
编辑src/uart_console.rs
,定义一个Writer结构,实现字节写入和字符串写入。
1 | //嵌入式系统使用串口,而不是vga,直接输出,没有颜色控制,不记录列号,也没有frame buffer,所以采用空结构 |
如何操作硬件通常需要阅读硬件制造商提供的技术手册。如pl011串口设备(PrimeCell UART)是arm设计的,其技术参考手册可以通过其官网查看。
依据之前
virt.dts
中的描述,pl011的寄存器在virt机器中被映射到了0x9000000
的内存位置。通过访问pl011的技术参考手册中Chapter 3. Programmers Model
中的Summary of registers
一节可知:第0号寄存器是pl011串口的数据寄存器,用于数据的收发。其详细描述参见 这里。注意到我们只是向UART0写入,而没从UART0读出(如果读出会读出其他设备通过串口发送过来的数据,而不是刚才写入的数据,这与读写内存时是不一样的,详情参见pl011的技术手册),编译器在优化时可能对这部分代码进行错误的优化,如把这些操作都忽略掉。
使用
ptr::write_volatile
库的目的是告诉编译器,这些写入有特定目的,不应将其优化(也就是告诉编译器不要瞎优化,这些写入和读出都有特定用途。比如连续两次读,编译器可能认为第二次读就是前次的值,所以优化掉第二次读,但对外设寄存器的连续读可能返回不同的值。
比如写,编译器可能认为写后没有读所以写没有作用,或者连续的写会覆盖前面的写,但对这些寄存器的写入对外设都有特定作用)。
在src/uart_console.rs
中为Write结构实现core::fmt::Write
trait,该trait会自动实现write_fmt
方法,支持格式化。
1 | //嵌入式系统使用串口,而不是vga,直接输出,没有颜色控制,不记录列号,也没有frame buffer,所以采用空结构 |
基于Rust的
core::fmt
实现格式化控制,可以使我们方便地打印不同类型的变量。实现core::fmt::Write
后,我们就可以使用Rust内置的格式化宏write!
和writeln!
,这使你瞬间具有其他语言运行时所提供的格式化控制能力。
在main.rs
末尾加入以下代码
1 | #![no_std] // 不使用标准库 |
编辑main.rs
中not_main
函数:
1 | pub extern "C" fn not_main() { |
编译并运行
1 | cargo build && qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
按住CTRL + A
,然后松手按C
,输入quit
即可退出QEMU模拟器。
现在我们已经可以采用
print_something
函数通过串口输出字符了。但若要实现输出,我们需要两个步骤:
(1)创建Writer类型的实例。
(2)调用实例的write_byte
或write_string
等函数。为了方便在其他模块中调用,我们希望可以直接执行步骤(2)而不是先执行步骤(1)再执行步骤(2)。
一般情况下可以通过将步骤(1)中的实例定义为
static
类型来实现,但Rust暂不支持Writer这样类型的静态(编译时)初始化,需要使用lazy_static
来解决。此外,为了保证访问安全还引入了自旋锁(spin)。
编辑Cargo.toml
:
1 | # ······ |
编辑src/uart_console.rs
,实现print!
和println!
宏。
1 | impl core::fmt::Write for Writer { |
在main.rs
中注释或删除之前的print_something()
函数及其调用,测试println!
宏
1 | // ······ |
编译并运行
1 | cargo build && qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
参考代码:下载
中断、异常和陷阱指令是操作系统的基石,现代操作系统就是由中断驱动的。本实验的目的在于深刻理解中断的原理和机制,掌握CPU访问设备控制器的方法,掌握ARM体系结构的中断机制和规范,实现时钟中断服务和部分异常处理等。
如下图所示,操作系统是一个多入口的程序,执行陷阱(Trap)指令,出现异常、发生中断时都会陷入到操作系统。
中断是一种硬件机制。借助于中断,CPU可以不必再采用轮询这种低效的方式访问外部设备。将所有的外部设备与CPU直接相连是不现实的,外部设备的中断请求一般经由中断控制器,由中断控制器仲裁后再转发给CPU。如下图所示ARM的中断系统。
其中nIRQ是普通中断,nFIQ是快速中断。ARM采用的中断控制器叫做GIC,即General Interrupt Controller。GIC包括多个版本,如GICv1(已弃用),GICv2,GICv3,GICv4。简单起见,我们实验将选用GICv2版本。
为了配置好GICv2中断控制器,与pl011串口一样,我们需要阅读其技术参考手册。
访问ARM官网下载ARM Generic Interrupt Controller Architecture version 2.0 - Architecture Specification。
从上图(来源于ARM Generic Interrupt Controller Architecture version 2.0 - Architecture Specification中的Chapter 2 GIC Partitioning)可以看出:
- GICv2最多支持8核的中断管理。
- GIC包括两大主要部分(由图中蓝色虚竖线分隔,Distributor和CPU Interface由蓝色虚矩形框标示),分别是:
- Distributor,其通过
GICD_
开头的寄存器进行控制(蓝色实矩形框标示)- CPU Interface,其通过
GICC_
开头的寄存器进行控制(蓝色实矩形框标示)- 中断类型分为以下几类(由图中红色虚线椭圆标示):
- SPI:(Shared Peripheral Interrupt),共享外设中断。该中断来源于外设,通过Distributor分发给特定的Core,其中断编号为32-1019。从图中可以看到所有核共享SPI。
- PPI:(Private Peripheral Interrupt),私有外设中断。该中断来源于外设,但只对指定的Core有效,中断信号只会发送给指定的Core,其中断编号为16-31。从图中可以看到每个Core都有自己的PPI。
- SGI:(Software-Generated Interrupt),软中断。软件产生的中断,用于给其他的Core发送中断信号,其中断编号为0-15。
- Virtual Interrupt,虚拟中断,用于支持虚拟机。图中也可以看到,因为我们暂时不关心,所以没有标注。
- 此外可以看到(FIQ,IRQ)可通过b进行旁路,我们也不关心。如感兴趣可以查看技术手册了解细节。
此外,由ARM Generic Interrupt Controller Architecture version 2.0 - Architecture Specification(Section 1.4.2)可知,外设中断可由两种方式触发:
- Edge-Triggered:边沿触发,当检测到中断信号上升沿时中断有效。
- Level-Sensitive:电平触发,当中断源为指定电平时中断有效。
因为SOC中中断有很多,为了方便对中断的管理,对每个中断附加了中断优先级。在中断仲裁时,高优先级的中断,会优于低优先级的中断,发送给CPU处理。当CPU在响应低优先级中断时,如果此时来了高优先级中断,那么高优先级中断会抢占低优先级中断,而被处理器响应。
由ARM Generic Interrupt Controller Architecture version 2.0 - Architecture Specification(Section 3.3)可知,GICv2最多支持256个中断优先级。GICv2中规定,所支持的中断优先级别数与GIC的具体实现有关,如果支持的中断优先级数比256少(最少为16),则8位优先级的低位为0,且遵循RAZ/WI(Read-As-Zero, Writes Ignored)原则。
1 | /* ······ */ |
由
virt.dts
中intc
和timer
的部分并结合kernel.org中关于ARM Generic Interrupt Controller和ARM architected timer的DeviceTree的说明可知:
intc
中的reg
指明GICD寄存器映射到内存的位置为0x8000000
,长度为0x10000
,GICC寄存器映射到内存的位置为0x8010000
,长度为0x10000
。intc
中的#interrupt-cells
指明interrupts包括3个cells。第一个文档指明:第一个cell为中断类型,0表示SPI,1表示PPI;第二个cell为中断号,SPI范围为**[0-987],PPI为[0-15];第三个cell为flags,其中[3:0]位表示触发类型,[4]表示高电平触发,[15:8]为PPI的CPU中断掩码,每1位对应一个CPU,为1表示该中断会连接到对应的CPU**。- 以
timer
设备为例,其中包括4个中断。以第2个中断的参数0x01 0x0e 0x104
为例,其指明该中断为PPI类型的中断,中断号14, 路由到第一个CPU,且高电平触发。但注意到PPI的起始中断号为16,所以实际上该中断在GICv2中的中断号应为16 + 14 = 30。
阅读ARM Generic Interrupt Controller Architecture version 2.0 - Architecture Specification,在Chapter 4 Programmers’ Model部分有关于GICD和GICC寄存器的描述,以及如何使能Distributor和CPU Interfaces**的方法。
访问ARM官网下载并阅读ARM Cortex-A Series Programmer’s Guide for ARMv8-A和AArch64 Exception and Interrupt Handling等技术参考手册。
ARMv8架构定义了两种执行状态(Execution States):AArch64和AArch32。分别对应使用64位宽通用寄存器或32位宽通用寄存器的执行。上图所示为AArch64中的异常级别(Exception levels)的组织。可见AArch64中共有4个异常级别,分别为EL0,EL1,EL2和EL3。在AArch64中,Interrupt是Exception的子类型,称为异常。AArch64中有四种类型的异常:
- Sync(Synchronous exceptions,同步异常)。在执行时触发的异常,例如在尝试访问不存在的内存地址时。
- IRQ (Interrupt requests,中断请求)。由外部设备产生的中断。
- FIQ (Fast Interrupt Requests,快速中断请求)。类似于IRQ,但具有更高的优先级,因此FIQ中断服务程序不能被其他IRQ或FIQ中断。
- SError (System Error,系统错误)。用于外部数据中止的异步中断。
当异常发生时,处理器将执行与该异常对应的异常处理代码。在ARM架构中,这些异常处理代码将会被保存在内存的异常向量表中。每一个异常级别(EL0,EL1,EL2和EL3)都有其对应的异常向量表。需要注意的是,与x86等架构不同,该表包含的是要执行的指令,而不是函数地址。异常向量表的基地址由
VBAR_ELn
给出,然后每个表项都有一个从该基地址定义的偏移量。 每个表有16个表项,每个表项的大小为128(0x80)字节(32条指令)。 该表实际上有4组,每组4个表项。 分别是:
- 发生于当前异常级别的异常且SPSel寄存器选择SP0,Sync、IRQ、FIQ、SError对应的4个异常处理。
- 发生于当前异常级别的异常且SPSel寄存器选择SPx,Sync、IRQ、FIQ、SError对应的4个异常处理。
- 发生于较低异常级别的异常且执行状态为AArch64,Sync、IRQ、FIQ、SError对应的4个异常处理。
- 发生于较低异常级别的异常且执行状态为AArch32,Sync、IRQ、FIQ、SError对应的4个异常处理。
新建src/interrupts.rs
,src/exceptions.s
1 | touch src/interrupts.rs src/exceptions.s |
编辑src/interrupts.rs
,定义各种常量,如寄存器地址和寄存器值等,然后定义init_gicv2
函数对GICD和GICC进行初始化,最后定义若干辅助函数用于中断配置。
1 | use core::ptr; |
编辑src/exceptions.s
,参照AArch64 exception table定义异常向量表。
1 | // SPDX-License-Identifier: MIT OR Apache-2.0 |
编辑src/interrupts.rs
,文末引入exceptions.s
,同时定义结构ExceptionCtx
,与src/exceptions.s
中EXCEPTION_VECTOR
宏保存的寄存器数据对应。
1 | // ······ |
继续编辑src/interrupts.rs
,在EXCEPTION_VECTOR
宏中,每一类中断都对应一个处理函数。
1 | // ······ |
编辑src/start.s
,载入异常向量表exception_vector_table
1 | // ······ |
编辑aarch64-qemu.ld
,处理链接脚本,为exceptions.s
中定义的exceptions_vector_table
选择位置,同时满足4K对齐。
1 | // ······ |
编辑src/main.rs
,引入interrupts.rs
模块,并在not_main()
函数中注释掉之前的输出代码,调用init_gicv2()
函数
1 | // ······ |
至此,我们已经在EL1级别定义了完整的中断处理框架,可以开始处理实际的中断了。
编辑src/interrupts.rs
,在init_gicv2
函数中添加使能时钟中断,同时配置时钟每秒产生一次中断。
1 | // ······ |
编译并以调试模式运行
1 | cargo build && qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -S -s |
保持此终端会话,新开一个终端,配置GDB环境
1 | cd ~/ToolChain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin |
启动GDB调试客户端
1 | aarch64-none-elf-gdb target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
连接远程客户端
1 | target remote localhost:1234 |
在not_main()
函数处设置断点
1 | b not_main |
运行到interrupts::init_gicv2();
语句之前。
1 | n |
我们之前在init_gicv2()
函数中加入了以下代码
1 | // 电平触发 |
因此,当我们运行init_gicv2()
函数后,其中的enable(TIMER_IRQ);
会产生使能中断。我们查看enable()
函数的代码
1 | // 使能中断号为interrupt的中断 |
由此可知,该函数对GICD_ISENABLER + interrupt / GICD_ISENABLER_SIZE
对应的地址易失性写入1 << (interrupt % GICD_ISENABLER_SIZE)
。
我们之前在src/interrupts.rs
中定义GICD
寄存器内存映射GICD_BASE
的起始地址为0x08000000
,而GICD_ISENABLER
的地址为GICD_BASE + 0x0100 = 0x08000100
,GICD_ISENABLER_SIZE
为32
,TIMER_IRQ
为30
。
1 | const GICD_BASE: u64 = 0x08000000; |
因此,对于enable(TIMER_IRQ);
,我们可以理解为在0x08000100
中易失性写入1左移30位后的二进制数。
查看0x08000100
地址中的值
1 | x/t 0x08000100 |
我们得到了0000000000000000111111111111111
,继续运行,执行interrupts::init_gicv2();
,再次查看0x08000100
地址中的值,此时变为了0100000000000000111111111111111
由此证明中断产生了。
实际上这里并没有产生中断!我们只是初始化了GICV2并且写入TIMER_IRQ中断号,如果时钟中断生效了,那么理论上来说每隔一秒都会调用一次el1_irq()
回调函数并且打印相应的中断信息,哪里出问题了呢?
参考代码:下载
在实现真正的时钟中断之前,我们的代码已经有亿点乱了,并且还会有很多恼人的unused warnings,因此我们先整理一下代码。
首先打开src/main.rs
:
注意到这里的core::ptr
并没有被使用。
因此我们将其移除。
not_main()
函数中移除不需要的代码
只保留一个println!宏
以及中断初始化函数init_gicv2()
即可。print_something()
函数我们亦不再用到,移除其相关代码。
现在看起来就清爽多了~
接下来在src/interrupts.rs
中有很多没有用到的常量和函数,通常称为dead_code
,但是为了保证完整性我们不选择删除它们,而是忽略掉。
在src/main.rs
中加入
1 | #![allow(dead_code)] // 忽略dead_code |
最后一个warning在aarch64-unknown-none-softfloat.json
中
1 | "abi-blacklist": [ |
这个abi-blacklist
推测是屏蔽一些接口,我们并没有调用这些接口,所以直接移除。
至此我们的warnings已经 全部处理(忽略) 完了。
参考代码:下载
在查阅大量的资料后,我找到了本次实验的原型(?)LeOS以及其对应的时钟中断部分的博客。仔细阅读可以发现他实现时钟中断的Commit。
在与noionion的合作及其帮助下,我们发现了LeOS关于时钟中断的实现与实验四 中断中有一些不一样的地方:
在初始化中断时,LeOS还多了以下代码:
因此我们在src/interrupts.rs
下的init_gicv2()
函数尾部添加以下代码:
1 | loop { |
这里的五句 asm! 其实前四句是无效操作,可以仅执行最后一句。
另外更严谨地说,该 loop {} 亦可放在 not_main() 函数中,调用 init_gicv2() 初始化后。
编译并运行
1 | cargo build && qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
在运行后,首先会输出
1 | [0] Hello from Rust! |
大约1s后会输出
1 | EL1_IRQ @ 0x |
说明这次我们成功调用了el1_irq()
回调函数!
但问题是,时钟中断的理想状态应是每隔1s就会调用一次el1_irq()
回调函数,这里调用一次后却不再变化了。
这里其实是因为catch()
函数在调用第一个参数ctx
时会发生阻塞,具体原因不详。
若想修复这个问题,可以编辑
.cargo/config.toml
,清空然后改为以下内容
1
2
3
4
5
6 [unstable]
build-std = ["core", "compiler_builtins"]
[build]
target = "aarch64-unknown-none-softfloat"
rustflags = ["-C","link-arg=-Taarch64-qemu.ld", "-C", "target-cpu=cortex-a53", "-D", "warnings"]
因此我们修改该函数,编辑interrupts.rs
:
1 | // ······ |
然后编译运行
可以发现现在确实能够一直触发中断并且输出回调函数名了。但这每次输出间隔的时间太短了吧!
这里我们需要了解一些概念:
详见此处
因此我们若想要有延时效果,需要在调用el1_irq()
回调函数时再次写入定时寄存器。
1 | asm!("mrs x1, CNTFRQ_EL0"); |
此时再编译运行,我们就已经成功做到每1s处理一次时钟中断了!
参考代码:下载
QEMU的virt机器默认没有键盘作为输入设备,但当我们执行QEMU使用
-nographic
参数(Disable graphical output and redirect serial I/Os to console)时QEMU会将串口重定向到控制台,因此我们可以使用UART作为输入设备。
在实验四中,针对GICD,GICC,TIMER等硬件我们定义了大量的常量和寄存器值,这在使用时过于繁琐也容易出错。因此我们决定采用tock-registers库。
在Cargo.toml
中的[dependencies]
处中加入依赖:
1 | tock-registers = "0.7.0" |
为了不至于使
src/uart_console.rs
文件过长,我们选择重构uart_console.rs
。
首先进入项目根目录,创建src/uart_console
目录
1 | mkdir src/uart_console |
将原uart_console.rs
更名为mod.rs
,且置于src/uart_console
目录下
1 | mv src/uart_console.rs src/uart_console/mod.rs |
最后新建src/uart_console/pl011.rs
1 | touch src/uart_console/pl011.rs |
依据tock_registers库的要求对pl011
所涉及到的寄存器进行描述。
编辑src/uart_console/pl011.rs
1 | use tock_registers::{registers::{ReadOnly, ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
register_bitfields!宏按照寄存器的位结构进行描述,注意最后要加分号”;”,只要注册自己想处理的位即可。
register_structs!宏最后需加上 (0x** => @END) ,表示结束。
编辑src/uart_console/mod.rs
,修改Writer的初始化方式
1 | // ······ |
编辑src/uart_console/mod.rs
,为Writer结构实现构造函数
1 | //嵌入式系统使用串口,而不是vga,直接输出,没有颜色控制,不记录列号,也没有frame buffer,所以采用空结构 |
继续编辑src/uart_console/mod.rs
,修改write_byte()
函数,使用我们通过宏描述的寄存器
1 | //嵌入式系统使用串口,而不是vga,直接输出,没有颜色控制,不记录列号,也没有frame buffer,所以采用空结构 |
编辑src/interrupts.rs
,修改init_gicv2()
函数,对UART的数据接收中断进行初始化:
1 | // ······ |
编辑src/interrupts.rs
,接下来我们定义UART0_IRQ
全局常量,同时把TIMER_IRQ
也修改为全局常量
1 | // 时钟中断号 |
继续编辑src/interrupts.rs
,对UART的数据接收中断进行处理,并修改timer中断的处理方法,使之每隔2秒输出一个点。
在文末添加以下三个函数。
1 | use tock_registers::interfaces::Readable; |
继续编辑src/interrupts.rs
,修改el1_irq()
函数
1 | #[no_mangle] |
之前我们修改了catch()
函数,没有调用ctx
参数,所以会有一个warning,这里我们选择再次将其忽略
编辑src/main.rs
1 | #![allow(dead_code, unused_variables)] // 忽略dead_code |
并且在src/uart_console/mod.rs
中,有一个未使用的core::ptr
引用,将其移除
接下来编译并运行
1 | cargo build && qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os |
可以看到每过2s就会打一个点。
如果我们按顺序输入a、b、c,则会触发输入中断
参考代码:下载
查看virt.dts
,可以发现:
1 | gpio-keys { |
其中gpio-keys中定义了一个poweroff键, gpios = <0x8003 0x03 0x00> 中的第一项0x8003表示它的phandle是0x8003, 即pl061@9030000,也即gpio-keys是设备pl061的组成部分,第二项0x03表示该键是pl061的第三根GPIO线,第三项是flag,且pl061的寄存器映射到了内存0x9030000开始的位置。如下图所示。
新建src/pl061.rs
1 | touch src/pl061.rs |
编辑src/pl061.rs
,通过tock-registers描述寄存器
1 | use tock_registers::{registers::{ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
编辑src/main.rs
,引入pl061模块。
1 | // ······ |
编辑src/interrupts.rs
,在init_gicv2
函数中初始化pl061的GPIO中断
1 | // ······ |
编辑src/interrupts.rs
,引入tock_registers::interfaces::Writeable
1 | // ······ |
编辑src/interrupts.rs
,处理pl061
3号GPIO线引发的中断
1 | // ······ |
在 handle_gpio_irq() 函数里通过内联汇编执行了指令hlt #0xF000,这里用到了ARM的Semihosting功能。
- Semihosting的作用:能够让bare-metal的ARM设备通过拦截指定的SVC指令,在连操作系统都没有的环境中实现POSIX中的许多标准函数,比如printf、scanf、open、read、write等等。这些IO操作将被Semihosting协议转发到Host主机上,然后由主机代为执行。
编辑src/interrupts.rs
,停止打点,方便后续测试观察
1 | // ······ |
为了启用Semihosting功能,在QEMU执行时需要加入 -semihosting 参数
1 | cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
先按Ctrl + A
,松手再按C
,然后输入system_powerdown
执行关机。
参考代码:下载
当多个任务访问同一个资源(数据)时就会引发竞争条件问题,这不仅在进程间会出现,在操作系统和进程间也会出现。由竞争条件引发的问题很难复现和调试,这也是其最困难的地方。本实验的目的在于了解竞争条件和死锁现象,并掌握处理这些问题的初步方法等。
在src/interrupts.rs的init_gicv2()函数中,我们之前使用了一个循环并使用内联汇编asm!(“wfi”)来等待中断,实际上在之前的实验中,这里所有的内联汇编都是没有必要的。当去掉这个循环,我们的OS会串行执行完成然后自动关机,从而导致后续的测试无效。因此我们需要且仅需要一个空循环来使OS持续运行,以便后续的中断测试。
编辑src/interrupts.rs
,在init_gicv2()
函数中移除loop{}
循环。
编辑src/main.rs
,在not_main()
函数尾部添加loop{}
空循环。
1 | // ······ |
首先编辑src/main.rs
,在not_main()
函数的空循环中调用print!
宏
1 | // ······ |
这里有两种方式复现死锁现象。
print!
宏与handle_uart0_rx_irq()
中print!
宏竞争检查src/interrupts.rs
中的handle_uart0_rx_irq()
函数,可以看到我们之前写了一个输入中断回调函数,在函数中调用了print!
宏输出信息。
直接编译并运行,预期在输入时触发死锁。
1 | cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
不停地乱序敲击键盘,此时有概率出现卡死,按键无法再次输入内容,即触发死锁现象。
print!
宏与handle_timer_irq()
中print!
宏竞争检查src/interrupts.rs
中的handle_timer_irq()
函数,可以看到我们之前写了一个时间中断回调函数,在函数中调用了print!
宏打点。
但它之前被我们注释掉了,因此我们取消注释
然后我们编译并运行,预期在打第一个点时会触发死锁。
1 | cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
实验按预期触发了死锁。
有时会在打第二个点时触发死锁。
编辑src/uart_console/mod.rs
,引入asm!
宏
1 | // ······ |
编辑src/uart_console/mod.rs
中的_print()
函数,在处理输入时先关闭中断,再打开。
1 | // ······ |
此时再用上述两种方式测试死锁,发现死锁现象消失了~
分页内存管理是内存管理的基本方法之一。本实验的目的在于全面理解分页式内存管理的基本方法以及访问页表,完成地址转换等的方法。
ARM Cortex-A Series Programmer’s Guide for ARMv8-A 中提到:
For EL0 and EL1, there are two translation tables. TTBR0_EL1 provides translations for the bottom of Virtual Address space, which is typically application space and TTBR1_EL1 covers the top of Virtual Address space, typically kernel space. This split means that the OS mappings do not have to be replicated in the translation tables of each task.
即TTBR0指向虚拟空间下半部分通常用于应用程序的空间,TTBR1指向虚拟空间上半部分通常用于内核的空间。其中TTBR0除了在EL1中存在外,也在EL2和EL3中存在,但TTBR1只在EL1中存在。
TTBR0_ELn和TTBR1_ELn是页表基地址寄存器,地址转换的过程如下所示
参考代码:下载
虚拟地址转换很容易出错也很难调试,所以我们从最简单的方式开始,即采用Identity Mapping,将虚拟地址映射到相同的物理地址。
编辑src/start.s
,初始化MMU、页表以及启用页表。
1 | .globl _start |
(如果预览不清晰,可以在新标签页中打开图片,或者下载图片,然后放大)
编辑aarch64-qemu.ld
,定义前文中用到的LD_TTBR0_BASE
和LD_TTBR1_BASE
符号
1 | ENTRY(_start) |
编译并运行,测试能否正常工作。
1 | cargo clean && cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
正常运行!
参考代码:下载
修改代码,将虚拟地址2G - 3G处映射到物理地址0 - 1G,从而对0x89000000地址的写入将通过pl011串口输出,因为此时0x89000000映射到了物理地址pl011@9000000。
编辑src/start.s
,空白映射虚拟地址0 - 1G,将虚拟地址2G - 3G处映射到物理地址0 - 1G。
1 | // ······ |
编辑src/interrupts.rs
,修改其基址(2G+原基址)
1 | use core::ptr; |
编辑src/pl061.rs
,修改其基址(2G+原基址)
1 | use tock_registers::{registers::{ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
编辑src/uart_console/pl011.rs
,修改其基址(2G+原基址)
1 | use tock_registers::{registers::{ReadOnly, ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
编译并运行,测试能否正常工作。
1 | cargo clean && cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
正常运行!
参考代码:下载
编辑src/start.s
,处理虚拟地址空间的上半部分。
1 | // ······ |
重构aarch64-qemu.ld
1 | __KERN_VMA_BASE = 0xfffffff000000000; |
编辑src/interrupts.rs
,修改其基址(0xfffffff000000000+原基址)
1 | use core::ptr; |
编辑src/pl061.rs
,修改其基址(0xfffffff000000000+原基址)
1 | use tock_registers::{registers::{ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
编辑src/uart_console/pl011.rs
,修改其基址(0xfffffff000000000+原基址)
1 | use tock_registers::{registers::{ReadOnly, ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
编译并运行,测试能否正常工作。
1 | cargo clean && cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
正常运行!
参考代码:下载
乍一看能正常运行,但是运行一段时间后居然卡死了!这是为什么呢?
第一时间想到的是互斥锁可能出问题了,但是仔细看互斥锁的代码发现和内存映射关系应该不大。
仔细观察输出发现打点是正常的,而且在没有卡死之前如果触发输入中断则会立刻卡死,因此判断是输入中断出了问题, noionion 认为是链接脚本的问题,而事实也如他所说。
编辑aarch64-qemu.ld
,修改.text : {}
部分:
1 | /* ······ */ |
编译并运行,测试能否正常工作。
1 | cargo clean && cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
正常运行!
参考代码:下载
在成功实现块级映射后,我们就可以尝试实现二级页表映射了。
编辑src/start.s
,修改块级映射为二级页表映射。
1 | // ······ |
编辑aarch64-qemu.ld
,定义LD_TTBR0_L2TBL
以及LD_TTBR1_L2TBL
符号
1 | /* ······ */ |
编译并运行,测试能否正常工作。
1 | cargo clean && cargo build && qemu-system-aarch64 -machine virt,gic-version=2 -cpu cortex-a57 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/rui_armv8_os -semihosting |
正常运行!
前前后后花了三个月时间完成了这篇1.6W字的笔记,最大的感悟却是:自己的挖的坑只能含泪填完。
]]>Android发展日新月异,由此诞生了许多强大的第三方库,例如Glide、OkHttp。我们当然也可以发布自己的库,当一回Wheel Maker!以前大家用Jcenter作为平台,而现在大家用MavenCentral。
MavenCentral由Sonatype运营,因此我们先 注册 一个 Sonatype账号。
首先填写项目和问题类型。
编辑这个问题,把GroupId改为io.github.xayahsususu并更新,然后将状态重新更新为开放,等待回复。
几分钟之后,正常情况下会受到完成的回复,注意回复中提到的s01.oss.sonatype.org
,这是我们管理上传库的地址,可能每个时期得到的地址不同。
问题状态变为已解决
发布到MavenCentral的库需要签名,因此我们 下载 相应工具来生成密钥,这里以Windows为例
安装Gpg4win,运行Kleopatra。
左上角文件 - 新建密钥对 - 创建个人 OpenPGP 密钥对
完成后在主界面打开生成的密钥,记住指纹后8位(后面要用到),然后生成吊销证书并保存
接下来回到主界面右键密钥 - 在服务器上发布,若网络没有问题即可发布成功。
发布成功后,再次右键密钥 - 备份私钥,将私钥导出(将后缀改为.gpg)。
接下来打开密钥,修改密码。
在项目根目录下新建publish-mavencentral.gradle
输入以下代码(如果上文中回复得到的地址不为s01.oss.sonatype.org
,则将其改为得到的地址)
1 | apply plugin: 'maven-publish' |
打开项目根目录中的local.properties
,添加以下代码
1 | signing.keyId=$密钥指纹后八位(无空格) |
打开待发布库目录下的build.gradle
,行位添加以下代码(根据示例参考修改填写)
1 | ext { |
打开Settings - Experimental - 取消勾选 Do not build Gradle task list during Gradle sync
点击左上角File - Sync Project with Gradle Files
完成后打开右侧Gradle窗口,双击运行build - assemble
编译完成后,双击运行publishing - publishReleasePublicationTo${LibraryName}Repository 发布到https://s01.oss.sonatype.org (具体地址由上文回复中可得)
打开管理地址后,进入Staging Repositories,即可看到我们上传的库记录。
打开库记录,点击Close,输入描述后Confirm,稍等片刻即可刷新查看,若Close成功,则点击Release发布。发布成功后,之前的问题会收到发布成功的回复。
假以时日,我们即可在https://repo1.maven.org/maven2/ 和 https://search.maven.org/ 中查看到我们发布的库信息。
打开需要引用库的项目的app目录下的build.gradle
,输入以下代码引用。
格式为:
1 | implementation '$PUBLISH_GROUP_ID:$PUBLISH_ARTIFACT_ID:$PUBLISH_VERSION' |
例如:
1 | implementation 'io.github.xayahsususu:materialyoufileexplorer:1.0.0' |
微软赛高!得益于其强大的技术支持,我们可以在 Windows 上无缝体验 Linux (Windows Subsystem for Linux) 甚至 Android (Windows Subsystem for Android) 。
将 WSL2 与 Jetbrains系列IDE 或 VS Code 结合,我们可以在 Windows 下无缝开发并调试 Linux 环境程序。
笔者使用的是 Windows 11 专业版。
打开 启用或关闭Windows功能
启用 适用于Linux的Windows子系统
下载并安装适用于 x64 计算机的 WSL2 Linux 内核更新包
1 | wsl --set-default-version 2 |
这里以Ubuntu 20.04 LTS为例
你还可以选择
打开Microsoft Store,搜索Ubuntu 20.04并安装
打开Ubuntu 20.04(可从Microsoft Store打开),配置用户名密码。
默认WSL位于C盘,如果使用频率较高的话,你会发现C盘占用空间蹭蹭往上涨~
这里笔者选择修改安装目录到D盘
1 | wsl -l --all -v |
1 | wsl --export Ubuntu-20.04 D:\WSL2\WSL-Ubuntu20.04.tar |
1 | wsl --unregister Ubuntu-20.04 |
1 | wsl --import Ubuntu-20.04 D:\WSL2\WSL-Ubuntu20.04 D:\WSL2\WSL-Ubuntu20.04.tar --version 2 |
1 | ubuntu2004 config --default-user $USERNAME |
1 | \\wsl.localhost\Ubuntu-20.04\home\$USERNAME |
使用Vim打开sources.list
1 | sudo vim /etc/apt/sources.list |
键入:%d
清空
粘贴阿里源代码
1 | deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse |
按Esc
,键入:wq
保存并退出
更新源并升级
1 | sudo apt-get update && sudo apt-get upgrade |
某些程序需要Systemd的支持,因此我们使用ubuntu-wsl2-systemd-script来开启。
安装git
1 | sudo apt-get install git |
克隆脚本仓库
1 | git clone https://github.com/DamionGans/ubuntu-wsl2-systemd-script.git |
跳转到ubuntu-wsl2-systemd-script
目录
1 | cd ubuntu-wsl2-systemd-script |
运行安装脚本
1 | bash ubuntu-wsl2-systemd-script.sh |
重启Ubuntu(宿主机终端执行)
1 | wsl --shutdown Ubuntu-20.04 |
再次登录到Ubuntu,验证Systemd
1 | systemctl |
如上图则配置成功
创建并编辑.proxyrc
文件
1 | vim .proxyrc |
输入以下代码($port
为宿主机代理端口,请自行修改)
1 | #!/bin/bash |
按Esc
,键入:wq
保存并退出
以后每次连接宿主机代理只需source .proxyrc
即可生效
Vue打包至移动端目前已经有许多解决方案,例如HBuilder、Cordova等等。但个人认为过程实在是太过于繁杂,且有的还需要联网验证,于是有了这篇文章。原理就是利用原生Android WebView本地加载Vue打包后的静态HTML,本文章以Android为例,使用Android Studio本地打包,IOS端同理。
工具:Android Studio
源码:npm run build
之后的静态资源,例如dist
文件夹
填写Name(应用名称)、Package Name(包名)、Language(以Kotlin为例)、Minimum SDK(应用支持的最低Android版本),然后点击Finish创建项目。初次安装AS并创建项目需要联网下载Gradle等依赖,请耐心等待。
默认主题会有一个ActionBar,需要去掉以保证加载后的Vue UI统一性。
首先在左侧打开themes下的两个主题文件,其中一个是深色模式的主题,我们需要将两个都改为NoActionBar。这里以浅色模式为例。
↓
同样,在浅色模式主题文件中,修改android:statusBarColor
为你想要的颜色,并将状态栏文字图标等设为深色
1 | <item name="android:windowLightStatusBar">true</item> |
深色模式同理。
打开activity_main.xml
:
删除TextView并添加WebView:
1 | <WebView |
在项目目录下app\src\main
创建assets
文件夹,将打包后的dist放入assets:
打开MainActivity.kt
绑定组件:
1 | val webView: WebView = findViewById(R.id.web_view) |
本地加载HTML:
1 | webView.loadUrl("file:////android_asset/dist/index.html") |
允许JS交互:
1 | webView.settings.javaScriptEnabled = true |
打开AndroidManifest.xml
:
添加INTERNET权限:
1 | <uses-permission android:name="android.permission.INTERNET" /> |
左栏右键res - drawable - New - Image Asset
配置图标前景、背景:
Next - Finish
删除ic_launcher.webp
、ic_launcher_round.webp
(如果有的话)
以真机调试为例,当然也可以使用模拟器。
真机调试需要在开发者模式开启USB调试模式。
点击右上方绿色三角形运行。
稍等片刻,会自动启动APP:
需要在开发者模式开启USB调试模式。
在MainActivity.kt
中添加以下代码:
1 | WebView.setWebContentsDebuggingEnabled(true) |
重新在AS中调试运行App,打开谷歌浏览器,地址栏输入:chrome://inspect/#devices
进入远程调试工具。
稍等片刻工具会自动载入已运行的WebView程序,点击inspect
进入调试模式。
点击工具栏 Build - Generate Signed Bundle / APK …
选择APK - Next
新建签名 Create new… 填写签名信息
选择刚刚创建的签名文件 xxx.jks 填写对应信息后点击 Next
选择release并点击Finish
项目目录下app\release
即是编译完成的apk文件。
建议从后端解决跨域问题,以Flask为例:
1 | pip3 install flask-cors |
1 | from flask import Flask |
打开AndroidManifest.xml
,在application
标签下配置:
1 | <application |
作者:覃超
链接:https://www.zhihu.com/question/28557115/answer/48094438
来源:知乎
REST – Resource Representational State Transfer 资源在网络中以某种表现形式进行状态转移
Resource :资源,即数据。例如 friends,profile等;
Representational :某种表现形式,例如JSON,XML,JPEG等;
State Transfer :状态变化。通过HTTP动词实现。
一言以蔽之:URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。
URL中只使用名词来指定资源,原则上不使用动词。
Server和Client之间传递某资源的一个表现形式,比如用JSON,XML传输文本,或者用JPG,WebP传输图片等。当然还可以压缩HTTP传输时的数据(On-wire data compression)。
用 HTTP Status Code传递Server的状态信息。例如最常用的 200 表示成功,500 表示Server内部错误等。
1 | # 获取某人的好友列表 |
1 | # 获取某人的详细信息 |
用HTTP协议里的动词来实现资源的添加,修改,删除等操作。即通过HTTP动词来实现资源的状态变化:
1 | # 删除某人的好友 |
1 | # 添加好友 |
1 | # 更新个人资料 |
如下使用不符合规范:
1 | GET /v1/deleteFriend |
老早就对这块神奇的微型电脑感兴趣,机缘巧合之下暂时搞到一块,我手上这块是树莓派3B。
起初我还以为我手上这块能够直接U盘引导,在我无数次尝试下终于放弃了这个想法,实际上 树莓派3B+ 才行。
因此只能用SD卡了!虽然卡能直接插在板子上,但烧录镜像的时候还是需要读卡器这一远古神器。
我的烧录环境是Windows。
下载并安装 Raspberry Pi Imager 。
打开Raspberry Pi Imager
首先选择镜像(官方的Raspberry Pi OS蛮好用的)
把SD卡插入读卡器,再连接到PC
然后选择存储卡
接下来点击Write,可以坐下来喝一杯奶茶并等待完成。
如果有HDMI接口和显示器,那么可以直接进行下一步,然后手动连接网络。
如果有网线,那么也可以直接进行下一步。
如果啥都没有(像我一样),那么就需要在开机前对烧录好系统的SD卡“动点手脚”了。
首先重新拔插读卡器
不要格式化H:(Windows无法识别Linux文件系统)
然后打开另一个boot分区
在该分区根目录下新建一个名为 ssh
的空白文件,以打开树莓派默认关闭的SSH功能
再在该分区下新建 wpa_supplicant.conf
,配置待连接的WIFI信息。
1 | country=CN |
最后,boot分区根目录新建文件如下:
到此,万事俱备,只欠东风。
将SD卡插在树莓派背后,再接上电源。
如果正常的话,此时树莓派红灯常亮(树莓派电源正常),绿灯闪烁(读写SD卡正常)。
在之前的网络配置正常的前提下,我们通过路由器可查看到树莓派的IP地址。
打开任意一款SSH工具(我这里用的是Xshell)
输入相应IP地址和端口(默认22)
接收主机密钥
默认账号: pi
默认密码: raspberry
至此,我们已成功开机并登录!
第一次启动Linux,首先要做的当然是换源啦!
这里以阿里源为例。
首先查看系统代号:
1 | lsb_release -a |
我手上这块树莓派OS系统代号是buster(如果是其他版本,请自行更换下述代码中的buster),修改两处源
1 | sudo nano /etc/apt/sources.list |
将之前的用 # 注释掉,再加入以下内容:
1 | deb http://mirrors.aliyun.com/raspbian/raspbian/ buster main contrib non-free rpi |
键入 Ctrl + O 保存
键入 Enter(回车) 确认文件名
键入 Ctrl + X 退出
1 | sudo nano /etc/apt/sources.list.d/raspi.list |
将之前的用 # 注释掉,再加入以下内容:
1 | deb http://mirrors.aliyun.com/raspbian/raspbian/ buster main |
键入 Ctrl + O 保存
键入 Enter(回车) 确认文件名
键入 Ctrl + X 退出
更新源并升级:
1 | sudo apt-get update && sudo apt-get upgrade |
选择Asia - ShangHai:
1 | sudo dpkg-reconfigure tzdata |
可得好好保护数据!关机时记得
1 | sudo shutdown -r now |
然后再断电!
]]>早已久仰Golang大名,以其优异的高并发支持著称。那么让我们来一探究竟吧!
在Golang官网下载并安装,目前较新的版本都能够自动配置环境变量。
但我们还需要配置一下Goproxy代理,以解决当前国内网络环境所带来的问题。
这里我推荐goproxy.io,那么我们就跟着它的文档配置吧。
1 | 1. 右键 我的电脑 -> 属性 -> 高级系统设置 -> 环境变量 |
工欲善其事,必先利其器。优秀的IDE能让我们开发效率直线上升。JetBrains系列的IDE深受大众喜爱。这里我们选择GoLand作为开发工具
下载GoLand并安装。
打开GoLand,新建一个工程,取名为HelloWorld,然后创建项目。
既然我们使用Gin框架来开发后端,那么肯定要先安装它
打开GoLand的终端(在项目根目录下打开系统终端也行),安装Gin-Gonic依赖
1 | go get -u github.com/gin-gonic/gin |
稍等片刻即可安装成功。
接下来我们创建一个Go文件,取名为HelloWorld.go。
键入以下代码:
1 | package HelloWorld |
点击GoLand右上方Add Configuration…
在弹出的窗口中点击左边的+号,选择Go Build,然后点击OK。
接下来我们点击IDE右上角绿色的右三角按钮运行。
报错了,查询一通资料后才发现,当我们只编译运行单文件时,包名必须为main,因此,我们修改代码:
1 | package main |
这下能够成功运行了。
我们打开浏览器,输入http://localhost:8080/
,即可看到项目正确运行。
定义数据库结构体
1 | type Doc struct { |
创建数据库并添加一条记录
1 | func main() { |
运行后,会在源码根目录创建doc.db,并且写入一条记录
1 | const ( |
1 | type Doc struct { |
1 | v1 := r.Group("/api/v1/docs") |
1 | func add(c *gin.Context) { |
1 | func get(c *gin.Context) { |
1 | package main |
这是一个很久之前的想法了,但是之前一直编译不成功。
这两天仔细研究了下,证明还是可行的。
Android NDK 版本:r23b
编译环境:Ubuntu 20.04
NDK目录:
~/NDK
想要交叉编译Git,需要先交叉编译Curl和Zlib,新版本的NDK已经包含预编译Zlib,所以我们只需要交叉编译Curl即可。
至于为什么要交叉编译Curl,是因为git clone命令在clone https等仓库时,需要依赖该库创建的git-remote-https等EFL文件。否则,在clone时会发生找不到remote-https等错误。
而若Curl依赖OpenSSL,因此还得先交叉编译OpenSSL。
下载 Git 、 Curl 和 OpenSSL 的源码并解压。
在OpenSSL官方仓库中,可以找到编译到Android平台的 文档 。
导出NDK临时变量
1 | export ANDROID_NDK_ROOT=~/NDK |
导出PATH临时变量
1 | export PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH |
配置Makefile
1 | ./Configure android-arm64 -D__ANDROID_API__=26 --prefix=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot |
编译
1 | make -j128 |
安装到prefix
目录
1 | make install -j128 |
打开Curl的源码,我们可以发现它提供了两种编译方式,Autoconf Makefile 和 Cmake。
使用Autoconf Makefile方式可以编译带OpenSSL的Curl,但是由于某种未知原因,在后面在编译Git时无法识别。
因此我们选择Cmake方式。
在INSTALL.cmake
中我们可以得到一些编译的信息:
1 | Current flaws in the curl CMake build |
由此可知,利用Cmake编译出来的Curl不带SSL库,但这并不影响我们后续编译Git和相应的git-remote-https等二进制文件。
cd到解压后的Curl根目录,输入:
1 | mkdir out && cd out |
接下来是我们的重头戏了。
进入Git源码解压后的根目录,可以发现Git有两种编译方式,一是非Autoconf Makefile,二是Autoconf Makefile,前者我尝试过多次,皆以失败告终。所以这次我们尝试后者。
我们的宿主机因为无法测试Android平台上的二进制文件,所以我们把测试代码删掉。
进入configure.ac
,删除:
1 | # |
再删除:
1 | # |
观察源码结构,可以发现并没有configure
文件,但存在configure.ac
,所以我们可以make一个configure
。
cd到源码根目录,输入:
1 | make configure |
此时即可生成configure
。
git编译时会默认编译pthread,而Android由于性能及安全原因,放弃了glibc在其平台上的支持,所以相应地交叉编译链也不含有这个库。
Android有其替代方案,所以我们在make
时将NEEDS_LIBRT
赋值为空从而不编译它即可。
1 | ifdef NEEDS_LIBRT |
而在configure.ac
中可以发现:
1 | AC_ARG_ENABLE([pthreads], |
所以我们可以使用 –disable-pthreads 参数来取消编译 phread 相关部分。
于是接下来我们输入以下命令
1 | export NDK=~/NDK # NDK根目录绝对路径 |
注意这里的--prefix
,需要定义为Android环境下git运行目录,否则会出现找不到templates等问题。
然后编译:
1 | make NEEDS_LIBRT= NO_TCLTK=1 -j128 |
注意这里的NO_TCLTK
,如果不定义它的话,会默认编译git-gui
,这不是我们需要的,所以将其定义为1
。
先安装到宿主机~/git/install
1 | make install NEEDS_LIBRT= NO_TCLTK=1 DESTDIR=~/git/install -j128 |
可得如下产物
1 | xayah@xayah-virtual-machine:~/git/install/data/local/tmp$ ls |
将~/git/install/data/local/tmp
打包并推送到Android /data/local/tmp
目录
由于一些魔法因素,打包为zip竟足足有200M+!可能是因为压缩算法不同,打包为tar.xz就会小很多,大概10M左右。
1 | PS C:\Users\Xayah\Desktop> adb push git.zip /data/local/tmp |
让我们git clone
试试
1 | cas:/data/local/tmp/bin $ ./git clone https://github.com/git/git mGit |
Segmentation fault!这可是个头疼的事。不过没有关系,我们可以使用 gdbserver 调试,看看到底是哪里出了问题。
首先将NDK中相应的gdbserver推送到Android平台,我的手机是aarch64(现在大多数安卓手机都是这个平台),也就是arm64,所以推送该文件夹下的gdbserver即可。
1 | adb push gdbserver /data/local/tmp/bin |
接下来新建一个终端并forward一个自定义端口:
1 | adb forward tcp:12345 tcp:12345 |
接下来在另一个终端中进入adb shell:
1 | ./gdbserver 0.0.0.0:12345 ./git clone https://github.com/git/git mGit |
现在gdbserver已经启动了,我们转向另一个终端,启动gdb:
现在gdb和gdbserver均已启动,我们指定目标端口:
输入c
继续运行
段错误出现了!可以看出错误是出在copy_gecos()
这个函数。
在源码中搜索这个函数,可以在ident.c
中找到。
定位至相应位置
这个函数看起来貌似和git config
有一丝关系,难道是因为我们没有定义git用户信息?
让我们试试
1 | ./git config --global user.name "Xayah" |
果然报错了!看来是.gitconfig
没有创建成功,在源码中搜索.gitconfig
,可以发现git获取.gitconfig
的路径是$HOME/.gitconfig
那么我们试试把$HOME
定义为当前目录
1 | export HOME=/data/local/tmp/bin |
再配置用户信息
1 | ./git config --global user.name "Xayah" |
这次没有报错了,并且当前目录也成功生成了.gitconfig
这次我们再试试git clone
1 | ./git clone https://github.com/git/git mGit |
成功!
]]>手机上也能运行Python?当然,我们只需要将其交叉编译到Android平台就可以了!
本次笔记将使用NDK的交叉编译链,为AArch64平台交叉编译Python3.9。
Android NDK 版本:r21e
编译环境:Ubuntu 21.04
解压Python3.9的源码
然后在根目录输入以下指令:
1 | export NDK=/home/xayah/NDK # NDK根目录绝对路径 |
然而,我们遇到了一个错误:
1 | configure: error: readelf for the host is required for cross builds |
看来是工具链没有配置完整,还差一个readelf,NDK的交叉编译链里已经集成了它,我们只需要加上去即可。
1 | export READELF=$TOOLCHAIN/bin/readelf |
再配置一次
1 | ./configure --host=$TARGET --build=aarch64 --disable-ipv6 ac_cv_file__dev_ptmx=no ac_cv_file__dev_ptc=no --prefix=/home/xayah/py/install |
这次配置成功了!
让我们进行接下来的编译吧:
1 | make -j128 && make install -j128 |
稍加等待,编译成功!
1 | xayah@xayah-virtual-machine:~/py/install$ pwd |
首先将编译产物打包再将其推送到Android平台中
1 | adb push py.zip /data/local/tmp/ |
由于Android对权限的限制,我们不能推送到任意目录,但可以将其推送到/data/local/tmp/
目录,因为它具有完整的文件操作权限。
1 | xayah@xayah-virtual-machine:~/py/install$ adb push py.zip /data/local/tmp/ |
推送成功,接下来我们进入adb shell
操作
1 | xayah@xayah-virtual-machine:~/py/install$ adb shell |
可以看到Python3.9成功的运行起来,并且输出Hello World!
]]>GitHub Page国内访问实在是太慢了!早就有了换地的想法,正好 B1ue1nWh1te 提醒了我, GitLab 和 Gitee 在国内的访问速度都不错,但是我实在不喜欢 Gitee 的UI,索性就换 GitLab 吧!
如果你没有自己的域名,那么可以创建一个名为${你的GitLab ID}.gitlab.io
的仓库,比如我的ID是 Xayah :
那么我可以新建一个名为xayah.gitlab.io
的仓库
创建好后,我们先把仓库 Clone 下来,然后把 Hexo项目 放进去。
public 文件夹是 Hexo 构建 静态页面 生成的,我们在服务器上不需要它,所以请检查你的.gitignore
中是否忽略了/public
,如果没有,请将其加入 忽略名单 。
配置好后,将本地仓库 Push 到 GitLab 。
接下来,我们要利用 GitLab 的 CI 为我们 构建静态网页 了。
在 Hexo项目根目录 新建一个名为.gitlab-ci.yml
的文件,
然后编辑该文件,复制并粘贴以下构建代码:
1 | image: node:14-alpine # use nodejs v14 LTS |
保存 后 Push 到仓库,正常情况下 GitLab CI 已经开始为我们进行 第一次构建 了,你可以在 仓库左侧 - CI/CD - Pipelines 里看到构建情况
稍等片刻,正常情况下即可构建成功。
那么此时访问${你的GitLab ID}.gitlab.io
就可以看到 构建成功 后的 博客页面 。
以后每次在本地创建文章、修改文章,Push到远程仓库后GitLab CI将会自动为你构建,几分钟后即可生效。
如果购买了 自己的域名 ,那么可以在 仓库左侧 - Settings - Pages 配置自己的 域名 。
点击右侧 New Domain 添加 域名 ,再根据提示在 域名提供商处 配置 DNS ,稍等片刻,访问 域名 ,即可访问 博客页面 。
]]>删除Commit(删除当前分支最新的一条Commit):
1 | 1) 本地删除Commit记录 |
从其他远程仓库导入项目
1 | 1) 首先将导入仓库克隆到本地 |
导入其他仓库的Commit记录
1 | 1) cd 到被导入项目根目录 |
修改最新的一条Commit记录信息
1 | 1) cd 到被导入项目根目录 |
导入其他仓库的分支
1 | 1) cd 到被导入项目根目录 |
修改历史Commit记录
1 | 1) 列出历史Commit列表(n为项数) |
修改历史Commit提交时间
1 | 1) 列出历史Commit列表(n为项数) |
撤回上次Commit并保留修改的文件(在Commit时恢复上次Commit信息):
1 | 1) 撤回上次Commit |
环境: 2021-1-12
系统: Ubuntu 20.04.1 LTS
网上类似的资料已经很多了,但由于 时效性 或多或少会有一些问题,我在这里记录下 当前环境 下成功同步的方法
安装必要工具
1 | sudo apt install curl |
1 | sudo apt install vim |
1 | sudo apt install git |
在 ~/bin 目录下载 repo :
1 | mkdir ~/bin |
1 | cd ~/bin |
1 | curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o repo |
1 | chmod +x repo |
配置 repo 环境变量:
1 | sudo vim ~/.bashrc |
输入 i
进入编辑模式
在 末尾 添加:
1 | export PATH=~/bin:$PATH |
1 | export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo' |
按 Esc
退出编辑模式,输入 :wq
保存并退出
使环境变量生效: source ~/.bashrc
在 清华/科大镜像源 下载 初始包 到 工作目录 (90GB左右):
在工作目录下解压:
1 | tar xf aosp-latest.tar |
经测试清华源下载的初始包直接 repo sync
会出现奇奇怪怪的问题,解决办法是:
显示隐藏文件 → 打开 aosp/.repo/manifests.git/config
修改其中的 url 为:
url = git://mirrors.ustc.edu.cn/aosp/platform/manifest
修改 符号链接 将 python3 默认指向 python 命令:
1 | sudo rm /usr/bin/python |
1 | sudo ln -s /usr/bin/python3 /usr/bin/python |
设置账号的 缺省身份标识 :
1 | git config --global user.email "you@example.com" |
1 | git config --global user.name "Your Name" |
回到 aosp 目录下同步源码: repo sync
成功 同步源码 !
从 Android6.0 开始,权限获取不再是简单地在 AndroidManifest.xml 添加几行代码了, Google 引入了 动态权限 的概念,需要在代码中添加。
AndroidManifest.xml 中添加权限
1 | <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
从 Android10 开始,还需要添加:
1 | <application |
在 Activity 相应位置调用:
1 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
然而在 Android11 开始,** WRITE_EXTERNAL_STORAGE** 等特殊权限的获取又发生了变化…
当仍沿用 Android10- 的权限获取方式时,会在调用权限时 抛出异常
因为在 Andorid11+ 中, Google 添加了一个新的权限: MANAGE_EXTERNAL_STORAGE
当 APP 动态请求权限时,会引导用户进入一个 权限设置界面
所以,在Android11+中:
AndroidManifest.xml中添加权限:
1 | <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> |
该权限仅支持 Android11+
在Activity相应位置调用:
1 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { |
所以合并起来的代码是:
1 | public void getWriteRight() { |