• 单片机到底是不是嵌入式?

    一定有很多人都听说过嵌入式和单片机,但在刚开始接触时,不知道大家有没有听说过嵌入式就是单片机这样的说法,其实嵌入式和单片机还是有区别的。单片机与嵌入式到底有什么关系? 下面我们就来说说嵌入式和单片机之间的联系和区别吧。 01 什么是单片机? 首先,我们来了解一下到底什么是单片机。 嵌入式系统的核心是嵌入式处理器。嵌入式处理器一般可以分为以下几种类型: 嵌入式微控制器MCU(Micro Control Unit) 嵌入式DSP处理器(Digital Signal Processor) 嵌入式微处理器MPU(Micro Processor Unit) 嵌入式片上系统SoC(System on Chip) 可编程片上系统SoPC(System on a Programmable Chip) 我们的单片机属于嵌入式微控制器MCU(Micro Control Unit) MCU内部集成ROM/RAM、总线逻辑、定时/计数器、看门狗、I/O、串口、A/D、D/A、FLASH等。典型代表如8051、8096、C8051F等。 单片机就是在一个芯片(Chip)上集成了CPU、SRAM、Flash及其他需要模块,在一个Chip上实现一个微型计算机系统,所以就叫Single Chip Microcomputer,也就是单片机了。 它其实就是一种集成电路芯片,是通过超大规模集成电路技术,将CPU、RAM、ROM、输入输出和中断系统、定时器/计数器等功能,塞进一块硅片上,变成一个超小型的计算机。 这么说来,单片机不就是一个嵌入式系统? 别急,我们往下看。 “单片机”其实是一种古老的叫法。在那个年代半导体工艺还在起步阶段,集成能力很差,往往是CPU一个芯片,SRAM一个芯片,Flash一个芯片,需要中断的话又得有个专门处理中断的芯片,所以一个完整可用的计算机系统是很多个芯片(Chip)做在一个PCB板上构成的。 不同的功能无法做进一个芯片(Chip),所以会有多片机。现在半导体技术早已非常发达,所以不存在多片机。但是,“单片机”的叫法却一直延用至今。 单片机技术从上世纪70年代末诞生,早期的时候是4位,后来发展为8位,16位,32位。它真正崛起,是在8位时代。8位单片机功能很强,被广泛应用于工业控制、仪器仪表、家电汽车等领域。 我们在研究单片机的时候,经常会听到一个词——51单片机。让我们来了解一下它究竟是什么。 51单片机,其实就是一系列单片机的统称。该系列单片机,兼容Intel 8031指令系统。它们的始祖,是Intel(英特尔)的8004单片机。 注意,51单片机并不全是英特尔公司产品。包括ATMEL(艾德梅尔)、Philips(飞利浦)、华邦Dallas(达拉斯)、Siemens(西门子)、STC(国产宏晶等公司,也有很多产品属于51单片机系列。 ATMEL公司的51单片机,AT89C51这是一个51单片机的开发板,中间那个芯片才是51单片机 51单片机曾经在很长时间里都是市面上最主流、应用最广泛的单片机,占据大量的市场份额。 51单片机其实放在现在毫无技术优势,是一种很老的技术。之所以它的生命力顽强,除了它曾经很流行之外,还有一个原因,就是英特尔公司彻底开放了51内核的版权。 所以,无论任何单位或个人,都可以毫无顾忌地使用51单片机,不用付费,也不用担心版权风险,所以很多学校也都在用这个。 此外,51单片机拥有雄厚的存量基础和群众基础。很多老项目都是用的51单片机,出于成本的考虑,有时候只能继续沿用51单片机的技术进行升级。 而且,很多老一辈的工程师,都精通51单片机开发技术。 所以,51单片机的生命力得以不断延续。 02 什么是嵌入式? 嵌入式系统是一种专用的计算机系统,作为装置或设备的一部分。通常,嵌入式系统是一个控制程序存储在ROM中的嵌入式处理器控制板。 事实上,所有带有数字接口的设备,如手表、微波炉、录像机、汽车等,都使用嵌入式系统,有些嵌入式系统还包含操作系统,但大多数嵌入式系统都是由单个程序实现整个控制逻辑。 从应用对象上加以定义,嵌入式系统是软件和硬件的综合体,还可以涵盖机械等附属装置。国内普遍认同的嵌入式系统定义为: 以应用为中心,以计算机技术为基础,软硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统。 嵌入式系统具体应用于哪些“专用”方向呢? 举例如下: 办公自动化:打印机,复印机、传真机 军事及航天类产品:无人机、雷达、作战机器人 家电类产品:数字电视、扫地机器人、智能家电 医疗电子类产品:生化分析仪血液分析仪、CT 汽车电子类产品:引擎控制、安全系统、汽车导航与娱乐系统 网络通信类产品:通信类交换设备、网络设备 (交换机、路由器、网络安全) 通信与娱乐:手机、数码相机、音乐播放器、可穿戴电子产品、PSP游戏机 工业控制类产品:工控机交互式终端 (POS、ATM)、安全监控、数据采集与传输、仪器仪表 上述这些领域,都使用了嵌入式系统。这还只是冰山一角。 可以说,嵌入式系统完完全全地融入了我们,时刻影响着我们的工作和生活。 嵌入式系统,既然是一个计算机系统,那么肯定离不开硬件和软件。 一个嵌入式系统的典型架构如下: 这里最重要的就是嵌入式操作系统和嵌入式微处理器。 从硬件角度来看,嵌入式系统就是以处理器(CPU)为核心,依靠总线(Bus)进行连接的多模块系统: 其实大家不难看出和个人PC是一样的方式。 单片机是有清晰定义的,就是单个片(chip)上的计算机系统。而不同的单片机虽然配置不同,性能不同,厂家不同,甚至指令集和开发方式不同,但是都是在一个片上的完整的计算机系统,这个定义不会错。 而嵌入式就是个不清晰的定义了,并没有非常明确的关于“嵌入式”这个词的定义。他也不像单片机一样,是个确定的“物”的名字。 03 单片机是不是嵌入式? 那么单片机到底是不是嵌入式呢? 简单来说:是。 因为很多嵌入式产品中被嵌入的计算机系统就是单片机,譬如空调中嵌入的控制板其实核心就是个单片机。实际上大部分家电产品中嵌入的计算机系统都是单片机。 因为单片机足够简单便宜而且够用,所以使用单片机是最划算最适合的。 而单片机现在出货量最大的领域也就是家电产品了,当然未来IOT类的应用会越来越多,会成为单片机的很大的增量市场。 04 广义和狭义的嵌入式 嵌入式这个概念实际上很泛化,现在讲嵌入式这个词的人,可能想表达的意思并不相同。咱们上面讲的嵌入式的概念是嵌入式本来的定义,也就是所谓广义上的嵌入式。 而狭义的嵌入式,其实是“嵌入式linux系统”的简称。 这种狭义的嵌入式最初指的是运行了linux系统的嵌入式计算机系统。后来也包括运行了和linux同级别的其他嵌入式系统(譬如WinCE、Vxworks、Android等)的计算机。 看过上面的介绍之后你就知道到底单片机是不是嵌入式了,其实这两者之间的联系有很深,总之,不管你是准备学习嵌入式或是单片机,都要自己想好了再做决定。 05 嵌入式和单片机的区别 说到这里,我们来看看,嵌入式和单片机的区别到底是什么。 从前文的介绍来看,嵌入式系统是一个大类,单片机是其中一个重要的子类。嵌式系统像是一个完整的计算机,而单片机更像是一个没有外设的计算机。 以前单片机包括的东西并不算多,两者的硬件区别较为明显。 但是,随着半导体技术的突飞猛进,现在各种硬件功能都能被做进单片机之中。所以,嵌入式系统和单片机之间的硬件区别越来越小,分界线也越来越模糊。 于是,人们倾向于在软件上进行区分。 从软件上,行业里经常把芯片中不带MMU(memory management unit,内存管理单元)从而不支持虚拟地址,只能裸奔或运行RTOS(实时操作系统,例如ucos、华为LiteOS、RT-Thread、freertos等)的system,叫做单片机(如STM32、NXP LPC系列、NXP imxRT1052系列等)。 同时,把芯片自带MMU可以支持虚拟地址,能够跑Linux、Vxworks、WinCE、Android这样的“高级”操作系统的system,叫做嵌入式。 在某些时候,单片机本身已经足够强大,可以作为嵌入式系统使用。它的成本更低,开发和维护的难度相对较小,尤其是针对一些针对性更强的应用。而嵌入式系统理论上性能更强,应用更广泛,但复杂度高,开发难度大。 06 我们为什么要学习嵌入式和单片机 今天我也只是给大家简单地介绍了一下单片机和嵌入式以及他们之间的关系和区别,虽然嵌入式系统已经有30多年的历史,但其实一直隐藏在背后的,自从物联网上升为国家战略后,嵌入式系统也渐渐从后台走到前台。 嵌入式和单片机并不是纯“硬件”类方向。如果你想学好嵌入式和单片机,只懂数字电路和微机接口这样的硬件知识是不够的,你更需要学习的,是汇编、C/C++语言、数据结构和算法知识。拥有软硬结合的能力,远远比单纯掌握某种程序开发语言更有价值。 其次,嵌入式和单片机拥有广泛的应用场景,在各个领域都有项目需求和人才需求。而且我们国家现在正在大力发展芯片产业,也会带动嵌入式人才的就业,提升待遇。 随着5G建设的深入,整个社会正在向“万物互联”的方向变革。 物联网技术也将迎来前所未有的历史机遇。嵌入式和单片机技术是物联网技术的重要组成部分,也将进入快速发展的时代。 技术越难,过程越苦,越有利于构建竞争壁垒。大学里很多同学都热衷于学习各种编程语言,往往忽视了这一块,可以说在嵌入式开发这一块的人才我们国家还是比较欠缺的。因此,我觉得大家非常值得投入时间去学习嵌入式开发的技能。原文:https://www.zhihu.com/question/315310041/answer/2179945564

    昨天 43浏览
  • 为什么​嵌入式开发全局变量要越少越好?

    嵌入式开发,特别是单片机os-less的程序,最易范的错误是全局变量满天飞。 这个现象在早期汇编转型过来的程序员以及初学者中常见,这帮家伙几乎把全局变量当作函数形参来用。 在.h文档里面定义许多杂乱的结构体,extern一堆令人头皮发麻的全局变量,然后再这个模块里边赋值123,那个模块里边判断123分支决定做什么。 每当看到这种程序,我总要戚眉变脸而后拍桌怒喝。没错,就是怒喝。 不否认全局变量的重要性,但我认为要十分谨慎地使用它,滥用全局变量会带来其它更为严重的结构性系统问题。 为什么全局变量要越少越好? 它会造成不必要的常量频繁使用,特别当这个常量没有用宏定义“正名”时,代码阅读起来将万分吃力。 它会导致软件分层的不合理,全局变量相当于一条快捷通道,它容易使程序员模糊了“设备层”和“应用层”之间的边界。写出来的底层程序容易自作多情地关注起上层的应用。 这在软件系统的构建初期的确效率很高,功能调试进度一日千里,但到了后期往往bug一堆,处处“补丁”,雷区遍布。说是度日如年举步维艰也不为过。 由于软件的分层不合理,到了后期维护,哪怕仅是增加修改删除小功能,往往要从上到下掘地三尺地修改,涉及大多数模块, 而原有的代码注释却忘了更新修改,这个时候,交给后来维护者的系统会越来越像一个“泥潭”,注释的唯一作用只是使泥潭上方再加一些迷烟瘴气。 全局变量大量使用,少不了有些变量流连忘返于中断与主回圈程序之间。 这个时候如果处理不当,系统的bug就是随机出现的,无规律的,这时候初步显示出病入膏肓的特征来了,没有大牛来力挽狂澜,注定慢性死亡。 无需多言,您已经成功得到一个畸形的系统,它处于一个神秘的稳定状态! 你看着这台机器,机器也看着你,相对无言,心中发毛。你不确定它什么时候会崩溃,也不晓得下一次投诉什么时候道理。 全局变量大量使用有什么后果? “老人”气昂昂,因为系统离不开他,所有“雷区”只有他了然于心。当出现紧急的bug时,只有他能够搞定。你不但不能辞退他,还要给他加薪。 新人见光死,但凡招聘来维护这个系统的,除了改出更多的bug外,基本上一个月内就走人,到了外面还宣扬这个公司的软件质量有够差够烂。 随着产品的后续升级,几个月没有接触这个系统的原创者会发现,很多雷区他本人也忘记了,于是每次的产品升级维护周期越来越长, 因为修改一个功能会冒出很多bug,而按下一个bug,会弹出其他更多的bug。在这期间,又会产生更多的全局变量。 终于有一天他告诉老板,不行啦不行啦,资源不够了,ram或者flash空间太小了,升级升级。 客户投诉不断,售后也快崩溃了,业务员也不敢推荐此产品了,市场份额越来越小,公司形象越来越糟糕。   要问对策,只有两个原则 能不用全局变量尽量不用,我想除了系统状态和控制参数、通信处理和一些需要效率的模块,其他的基本可以靠合理的软件分层和编程技巧来解决。 如果不可避免需要用到,那能藏多深就藏多深。 如果只有某.c文件用,就static到该文件中,顺便把结构体定义也收进来; 如果只有一个函数用,那就static到函数里面去; 如果非要开放出去让人读取,那就用函数return出去,这样就是只读属性了; 如果非要遭人蹂躏赋值,好吧,我开放函数接口让你传参赋值; 实在非要extern侵犯我,我还可以严格控制包含我.h档的对象,而不是放到公共的includes.h中被人围观,丢人现眼。 如此,你可明白我对全局变量的感悟有多深刻,悲催的我,已经把当年那些“老人”交给我维护的那些案子加班全部重新翻写了。 最后补充 全局变量是不可避免要用到的,每一个设备底层几乎都需要它来记录当前状态,控制时序,起承转合。但是尽量不要用来传递参数,这个很忌讳的。 尽量把变量的作用范围控制在使用它的模块里面,如果其他模块要访问,就开个读或写函数接口出来,严格控制访问范围。 这一点,C++的private属性就是这么干的,这对将来程序的调试也很有好处。 C语言之所以有++版本,很大原因就是为了控制它的灵活性,要说面向对象的思想,C语言早已有之,亦可实现。 当一个模块里面的全局变量超过3个(含)时,就用结构体包起来吧,要归0便一起归0,省得丢三落四的。 在函数里面开个静态的全局变量,全局数组,是不占用栈空间的,只是有些编译器对于大块的全局数组,会放到和一般变量不同的地址区。 若是在keil C51,因为是静态编译,栈爆掉了会报警,所以大可以尽情驰骋,注意交通规则就是了。 单片机的os-less系统中,只有栈没有堆的用法,那些默认对堆分配空间的“startup.s”,可以大胆的把堆空间干掉。 程序模型?如何分析抽象出来呢,从哪个角度进行模型构建呢?很愿意聆听网友的意见。 本人一直以来都是从两个角度分析系统,事件--状态机迁移图 和 数据流图,前者分析控制流向,完善UI,后者可知晓系统数据的缘起缘灭。 这些理论,院校的《软件工程》教材都有,大家不妨借鉴下。只不过那些理论,终究是起源于大型系统软件管理的,牛刀杀鸡,还是要裁剪一下的。 

    昨天 40浏览
  • 脑洞有多大,MCU就能玩得有多花

    都说MCU本身不算什么高级东西,在MCU开发过程中,需要按照一定的标准化来执行,比如对变量,函数的定义,要确定他的生命周期,调用范围,访问条件等;常用的通信协议读写的协议往往应该抽象化,规定固定的输入输出,方便产品移植。 但实际上,很多时候,针对同一个需求其实有多种实现方案,但总有一个最优解。所以在这个过程中,总会有一些“脑洞大开”的操作,为人提供很多思路,今天就举几个例子给大家作为参考。 那些很惊艳的用法 当需要通过串口接收一串不定长数据时,可以使用串口空闲中断;这样就可以避免每接收到一个字符就需要进入中断进行处理,可以减少程序进入中断次数从而提高效率。 当需要测量一个波形的频率时,很多人会选择外部中断,其实通过定时器的外部时钟输入计数波形边沿,然后定时读取计数值计算频率的方式可以大大减少中断触发频率,提高程序执行效率。 在处理复杂的多任务场景时,可以利用实时操作系统(RTOS)来管理任务调度,提高系统的响应性和资源利用率。 对于需要低功耗运行的场景,可以采用动态电压频率调整(DVFS)技术,根据系统负载实时调整 MCU 的工作电压和频率,以降低功耗。 在进行数据存储时,采用闪存的磨损均衡算法,延长闪存的使用寿命。 利用硬件加密模块(如 AES 加密引擎)来保障数据的安全性和保密性,而不是通过软件实现加密,提高加密效率和安全性。 对于传感器数据的处理,采用数字滤波算法(如卡尔曼滤波),提高数据的准确性和稳定性。 当需要与多个设备进行通信时,采用总线仲裁机制和优先级设置,确保通信的高效和稳定。 在进行电源管理时,通过监测电源电压和电流,实现智能的电源管理策略,例如在低电量时进入低功耗模式。 对于实时性要求极高的控制任务,采用硬件直接触发中断,而不是通过软件轮询,减少响应延迟。 在单片机上跑的任何非线性系统的动态控制,都是高级用法。 用单片机去实现某种特殊的运动控制,赚很多钱,就是高级用法。 GPIO模拟一切 名为ShiinaKaze的网友,就非常“勇”,做了一个很折磨的事。 他用STM32F1利用GPIO模拟摄像头接口驱动OV2640摄像头模块。他表示,这是一个很折磨人的过程,我最多优化到了 1.5 FPSQ,所以选型一定要选好,不要折磨自己。设备采用STM32F103C8T6,OV2640,实现效果如下: OV2640实际时序图: 这个项目难点在于: 1.SCCB 模拟:SCCB 是12C-bus 的改版,主要是 OV2640 模块没有上拉电阻,无法进行通信,花了好长时间才发现这个问题; 2.并行接口的模拟:如果使用 IO 模拟的话,只能达到1FPS,但是使用了 Timer 和 DMA,就可以达到 1.5~2 FPS。 关于 image sensor 的数据接收和处理的问题背景:现有 ov2640 image sensor,接口为 DCMI(并行接口)问题:现有 STM32H7 想获取 OV2640 的 mjpeg 流数据,并通过传输数据到 PC 软件 1.采用 USART 还是 USB? 2.接收数据选择哪种中断,Line interrupt 还是 Frame interrupt ? 3.DCMI 通过 DMA 将数据转到 RAM 中的 Buffer,那么 Buffer 该如何设计,是设置一块大的连续 buffer?还是需要做一个 ring buffer,避免数据覆盖和数据顺乱? 4.触发中断后,是否关闭 DCMI 和 DMA ? 嵌入式软件架构挺重要的,特别是大型项目。这是 STM32 的软件架构,不知道各位还有没有其他架构。 有网友吐槽,你要是在学校,我敬你是条汉子,你要是在工作岗位上干这鸟事,那你们的架构也太坏了。而他也表示——“我错了,再也不模拟了。” 关于MCU不一样的观点 虽然如此,很多人还是认为,MCU不高级,使用单片机也不高级。高级的内容都是可以发论文的,使用单片机发不了论文。但使用单片机解决指定的任务,这很高级。 尤其是上面所说的一些例子,确实是MCU外设的一些高端玩法。只不过,这些机制可能只是一种标准用法。名为lion187的网友就表示,毕竟许多硬件机制有实际需求后才添加进来的,比如接收不定长数据,最初没有超时中断的情况下只能软件实现,极大的浪费了CPU的效率,所以才设计了超时中断来减少软件工作量,进而形成了一种标准使用方法。 当然,这也是芯片设计和制造工艺的提升带来的红利,早期芯片设计和工艺无法满足复杂外设电路时,谁也不敢会去想用硬件来实现这么复杂的功能,任何产品的开发,都离不开具体业务需求,MCU也不例外, 对产品来说,MCU外设的驱动只是完成开发的基本要素,更多的工作是围绕着业务逻辑展开的应用程序的开发。这时候数据结构与算法,各种控制算法和数值计算方法,设计模式,软件工程和设计理念成了高级的东西。 比如说,Linux 内核中的各驱动子系统的设计,设备对象和驱动对象这些沿用了 C++ 面向对象编程的思路,其实也可以沿用到 MCU的开发中,将设备与驱动分离,就可以使用同一套驱动算法来实现同类设备的不同驱动方法, 比如:同一个 UART 驱动可以根据配置的不同来驱动 UARTO,也可以驱动 UART1,而且波特率也可以不同(只要为 UART 类创建不同的实例对象就可以了,用 C 语言就行),这就是 C++ 中方法与属性分离带来的好处。 同样在业务应用部分,单件模式、工厂模式等设计模式,状态机模型的使用也会给开发带来很多便利,使系统结构清晰,有效减少Bug数量,且易于维护和扩展。 当然,也有人认为,论高级还得是FPGA。就比如AMD(赛灵思)的ZYNQ,当你需要通过串口接收一串不定长数据时,可以直接用Programmable Logic部分写一个专用的,最终结果放到DRAM里,发个信号通知ARM处理器来读就好了;当你需要测量一个波形的频率时,可以直接用Programmable Logic部分写一个专用的,实时不间断测量。这就很高级。 所以,对此你有什么看法,你有什么很“高级”的用法想要分享? 

    昨天 22浏览
  • STM32时钟系统的学习笔记

    STM32单片机时钟相关知识

    昨天 20浏览
  • 基于STM32的室内温湿度采集控制系统

    Proteus仿真——《基于STM32的室内温湿度采集控制系统》

    昨天 28浏览
  • 欢迎志同道合的小伙伴加入电子学习群

    由于之前的群都是复刻为主,很多小伙伴们在学习的过程中都不知道在哪个群里提问比较好,故新建一个专门的电子学习交流群,方便小伙伴们!获取入群二维码的方式,如下:关注本人微信公众号“核桃设计分享”后,点击发消息。然后在左下角点击:学习群点击后会出现如下界面:最后点击电子学习交流群即可获得入群二维码了欢迎志同道合的你加入!

    昨天 21浏览
  • STM32最小系统板电路知识学习

    STM32最小系统板电路知识学习 单片机最小系统是指用最少的电路组成单片机可以工作的系统,通常最小系统包含:电源电路、时钟电路、复位电路、调试/下载电路,对于STM32还需要启动选择电路。总之,刚开始如果不太懂电路的话,就抄别人的电路,然后自己拼凑。下图为stm32c8t6经典电路原理图 文章目录 STM32最小系统板电路知识学习 一、电源转换电路 二、JTAG/SWD调试接口电路 三、时钟电路 四、复位电路 提示:以下是本篇文章正文内容,下面案例可供参考 一、电源转换电路 开发板通常采用USB供电,通常USB都为5V,因此需要将5V转换成3.3V,使用TPS73633或者AMS1117芯片电源芯片即可实现。 首先设计电源入口部分,现在大多数开发板所使用的都是USB的5V供电,所以我们本次设计也采用USB接口供电,所以我们电源接口就采用5Pin的mini贴片的USB,将5V的电源引入开发板使用,其电路图如下,1脚为电源正极,5脚为负极,串接的二极管是为了保护我们的开发板,防止有个别的连接线极性不对烧坏板子,保护电路在我们设计任何电路时都要考虑到,这个大家以后自己设计时也要注意。这样我们就可以通过连接线将5V的USB电源引入到开发板中进行使用了。 接下来便是电源电路,STM32工作电压是DC3.3V,所以我们需要一个能将大于3.3V电压转换为稳定的3.3V电压的芯片,这里我们使用的是TPS73633或者AMS1117芯片电源芯片即可实现。 下图为TPS73633芯片的相关说明,TPS73633DBVR是一款3.3V固定输出低压降(LDO)线性稳压器,采用了一种新的拓扑-电压跟随器配置中的NMOS调整元件。使用具有低ESR的输出电容器,这种拓扑是稳定的,甚至可以在没有电容器的情况下运行。它还提供高反向阻塞(低反向电流)和接地引脚电流,该电流在所有输出电流值上都几乎恒定。该器件使用先进的BiCMOS工艺来产生高精度,同时提供非常低压降(LDO)的电压和低接地引脚电流。未启用时,电流消耗低于1uA,非常适合便携式应用。极低的输出噪声非常适合为VCO供电。该器件受热关断和折返电流限制保护。 二、JTAG/SWD调试接口电路 JTAG/SWD调试接口电路采用了标准的JTAG接法,这种接法兼容SWD接口,因为SWD只需要四根线(SWCLK、SWDIO、VCC和GND)。需要注意的是,该接口电路为JLINK或ST-Link提供3.3V的电源,因此,不能通过JLINK或ST-Link对STM32核心板进行供电,而是STM32核心板为JLINK或ST-Link供电。JLINK和ST-Link不仅可以下载程序,还可以对STM32微控制器进行在线调试。 三、时钟电路 MCU是一个集成芯片,由非常复杂的数字电路和其它电路组成,需要稳定的时钟脉冲信号才能保证正常工作。时钟如同人体内部的心脏一样,是芯片的“动力”来源。时钟产生一次,就推动处理器执行一下指令。除了CPU,芯片上所有的外设(GPIO、I2C、SPI等)都需要时钟,由此可见时钟的重要性。芯片运行的时钟频率越高,芯片处理的速度越快,但同时功耗也越高。为了功耗和性能兼顾,微处理器一般有多个时钟源,同时还将时钟分频为多个大小,适配不同需求的外设。下图为stm32的时钟树 这里我们将两个晶振电路,电源,以及各引脚的网络符号对应连接好即可,除去晶振和电源,其余的标号都是连接在我们引出的排针上边的,晶振电路这里包含了一个8MHz的主晶振,以及一个32.768kHz的内部RTC实时时钟晶振,这里时钟晶振作为预留,如果有用到时钟的小伙伴直接焊接上即可,方便使用,每个晶振后边并联的为起振电容,方便晶振起振,电源部分的电容C3-C7组成了一个低通滤波电路,目的是为了让32更好的工作 四、复位电路 嵌入式系统中,由于外界环境干扰,难免出现程序跑飞或死机,这时就需要复位让MCU重新运行。该电路将一个按键接在了NRST引脚,一旦按键按下,NRST就会接地,拉低NRST,实现复位。

    昨天 43浏览
  • kprobes的技术实现原理,实现方式及应用

    Linux kprobe调试技术是内核开发者专门为了编译跟踪内核函数执行状态所涉及的一种轻量级内核调试技术,利用kprobe技术,内核开发人员可以在内核的绝大多数指定函数中动态插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。本章的是基于5.15内核来学习kprobe的相关内容,主要包括以下内容 kprobe技术产生的背景 主要针对ARM64 kprobes的技术实现原理,实现方式 对于ftrace中的kprobe是如何实现的 kpobe可以做什么,可以解决哪些问题 1 kprobe技术背景 对于开发者,我们在内核或者模块的调试过程中,往往需要知道一些函数的执行流程,何时被调用,执行过程中的入参和返回值是什么等等,比较简单的做法就是在内核代码对应的位置添加日志信息,但是这种方式往往需要重新编译内核或者模块,烧写或者替换模块,操作较为复杂甚至可能会破坏原有的代码执行过程。 所以针对这种情况,内核提供了一种调试机制kprobe,提供了一种方法,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行。 它的基本工作原理是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。 kprobe 是一种动态调试机制,用于debugging,动态跟踪,性能分析,动态修改内核行为等,2004年由IBM发布,是名为Dprobes工具集的底层实现机制,2005年合入Linux kernel。probe的含义是像一个探针,可以不修改分析对象源码的情况下,获取Kernel的运行时信息。kprobe一直在X86系统上使用,ARM64的平台支持在2015年合入kernel ,kprobe提供了三种形式的探测点 一种最基本的kprobe:能够在指定代码执行前,执行后进行探测,但此时不能访问被探测函数内的相关变量信息,内核代码的任何指令处 一种是jprobe:用于探测某一个函数的入口,并且能够访问对应的函数参数,这个目前已经不再使用 一种是kretprobe:用于完成指定函数返回值的探测功能,内核函数的退出点 其中最基本的就是kprobe机制,jprobe以及kretprobe的实现都依赖于kprobe,kprobe是linux内核的一个重要的特性,是其他内核调试工具(perf,systemtap)的基础设施,同时内核BPF也是依赖于kprobe,它是利用指令插桩原理,截获指令流,并在指令执行前后插入hook函数,其如下: 所以kprobe的实现原理是把制定地址(探测点)的指令替换成一个可以让cpu进入debug模式的指令,使执行路径暂停,跳转到probe处理函数后收集,修改信息,然后再跳转回来继续执行的过程。 如果需要知道内核函数是否被调用、被调用上下文、入参以及返回值,比较简单的方式是加printk,但是效率低,利用kprobe技术,用户可以自定义自己的回调函数,可以再几乎所有的函数中动态插入探测点。 首先kprobe是最基本的探测方式,是实现后两种的基础,它可以再任意的位置放置探测点(就连函数内部的某条指令处也可以),提供了探测点的调用前,调用后和内存访问出错3种回调方式,分别是- - per_handler:将在被探测指令执行前回调 post_handler:将在被探测指令执行完毕后回调(注意不是被探测函数) 对于kretprobe从名字就可以看出,它同样是基于kprobe实现,用于获取被探测函数的返回值 2 ARM64 kprobe的工作原理 实现kprobes 接口的数据结构和函数已在文件中定义。下面的数据结构描述了一个 kprobe struct kprobe { struct hlist_node hlist; /* 所有注册的kprobe都会添加到kprobe_table哈希表中,hlist成员用来链接到某个槽位中 */ /* list of kprobes for multi-handler support */ struct list_head list; /* 链接一个地址上注册的多个kprobe */ /*count the number of times this probe was temporarily disarmed */ unsigned long nmissed; /* 记录当前的probe没有被处理的次数 */ /* 一个是用户在注册前指定探测点的基地址(加上偏移得到真实的地址), * 另一个是在注册后保存探测点的实际地址, 如果没有指定,必须指定探测的位置的符号信息 */ /* location of the probe point */ kprobe_opcode_t *addr; /* 探测点地址 */ /* 名称和地址不能同时指定,否则注册时会返回EINVAL错误 */ /* Allow user to indicate symbol name of the probe point */ const char *symbol_name; /* 探测点函数名 */ /* Offset into the symbol */ unsigned int offset; /* 探测点在函数内的偏移 */ /* 断点异常触发之后,开始单步执行原始的指令之前被调用 */ /* Called before addr is executed. */ kprobe_pre_handler_t pre_handler; /* 在单步执行原始的指令后会被调用 */ /* Called after addr is executed, unless... */ kprobe_post_handler_t post_handler; /* 后处理函数 */ /* 原始指令,在被替换为断点指令(X86下是int 3指令)前保存。*/ /* Saved opcode (which has been replaced with breakpoint) */ kprobe_opcode_t opcode; /* copy of the original instruction */ struct arch_specific_insn ainsn; /* 保存平台相关的被探测指令和下一条指令 */ /* * Indicates various status flags. * Protected by kprobe_mutex after this kprobe is registered. */ u32 flags; /* 状态标记 */}; 所以对于kprobe的使用比较简单,只需要指定探测点地址,或者使用符号名+偏移的方式,定义xxx_handler,注册即可,注册后,探测指令被替换,可以使用kprobe_enable/disable函数动态开关 2.1 kprobe初始化 下面我们来看看 kprobe 的初始化过程,kprobe 的初始化由 init_kprobes() 函数kernel/kprobes.c实现: static int __init init_kprobes(void){ int i, err = 0; /* 初始化用于存储 kprobe 模块的哈希表 */ /* FIXME allocate the probe table, currently defined statically */ /* initialize all list heads */ for (i = 0; i < KPROBE_TABLE_SIZE; i++) INIT_HLIST_HEAD(&kprobe_table[i]); /* 初始化 kprobe 的黑名单函数列表(不能被 kprobe 跟踪的函数列表) */ err = populate_kprobe_blacklist(__start_kprobe_blacklist, __stop_kprobe_blacklist); if (err) { pr_err("kprobes: failed to populate blacklist: %d\n", err); pr_err("Please take care of using kprobes.\n"); } if (kretprobe_blacklist_size) { /* lookup the function address from its name */ for (i = 0; kretprobe_blacklist[i].name != NULL; i++) { kretprobe_blacklist[i].addr = kprobe_lookup_name(kretprobe_blacklist[i].name, 0); if (!kretprobe_blacklist[i].addr) printk("kretprobe: lookup failed: %s\n", kretprobe_blacklist[i].name); } } /* By default, kprobes are armed */ kprobes_all_disarmed = false; #if defined(CONFIG_OPTPROBES) && defined(__ARCH_WANT_KPROBES_INSN_SLOT) /* Init kprobe_optinsn_slots for allocation */ kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE;#endif /* 初始化CPU架构相关的环境(x86架构的实现为空) */ err = arch_init_kprobes(); if (!err) err = register_die_notifier(&kprobe_exceptions_nb); /* 注册die通知链*/ if (!err) err = register_module_notifier(&kprobe_module_nb); /* 注册模块通知链 */ kprobes_initialized = (err == 0); if (!err) init_test_probes(); return err;}early_initcall(init_kprobes); 2.2 注册一个kprobe实例 内核是通过register_kprobe完成一个kprobe实例的注册,其详细实现过程在kernel/kprobes.c,如下所示 /* struct kprobe结构体,里面包含指令地址或者函数名地址和函数内偏移 */int register_kprobe(struct kprobe *p){ int ret; struct kprobe *old_p; struct module *probed_mod; kprobe_opcode_t *addr; /* 获取被探测点的地址,指定了sysmbol name,则kprobe_lookup_name从kallsyms中获取; * 指定了offsete + address,则返回address + offset */ /* Adjust probe address from symbol */ addr = kprobe_addr(p); if (IS_ERR(addr)) return PTR_ERR(addr); p->addr = addr; /* 判断同一个kprobe是否被重复注册 */ ret = warn_kprobe_rereg(p); if (ret) return ret; /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */ p->flags &= KPROBE_FLAG_DISABLED; p->nmissed = 0; INIT_LIST_HEAD(&p->list); /* 1. 判断被注册的函数是否位于内核的代码段内,或位于不能探测的kprobe实现路径中 * 2. 判断被探测的地址是否属于某一个模块,并且位于模块的text section内 * 3. 如果被探测的地址位于模块的init地址段内,但该段代码区间已被释放,则直接退出 */ ret = check_kprobe_address_safe(p, &probed_mod); if (ret) return ret; mutex_lock(&kprobe_mutex); /* 判断在同一个探测点是否已经注册了其他的探测函数 */ old_p = get_kprobe(p->addr); if (old_p) { /* Since this may unoptimize old_p, locking text_mutex. */ /* 如果已经存在注册过的kprobe,则将探测点的函数修改为aggr_pre_handler * 将所有的handler挂载到其链表上,由其负责所有handler函数的执行 */ ret = register_aggr_kprobe(old_p, p); goto out; } cpus_read_lock(); /* Prevent text modification */ mutex_lock(&text_mutex); /* 分配特定的内存地址用于保存原有的指令 */ ret = prepare_kprobe(p); mutex_unlock(&text_mutex); cpus_read_unlock(); if (ret) goto out; /* 将kprobe加入到相应的hash表内 */ INIT_HLIST_NODE(&p->hlist); hlist_add_head_rcu(&p->hlist, &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); /* 将探测点的指令码修改为arm_kprobe */ if (!kprobes_all_disarmed && !kprobe_disabled(p)) { ret = arm_kprobe(p); if (ret) { hlist_del_rcu(&p->hlist); synchronize_rcu(); goto out; } } /* Try to optimize kprobe */ try_to_optimize_kprobe(p);out: mutex_unlock(&kprobe_mutex); if (probed_mod) module_put(probed_mod); return ret;} 其主要包括以下几个步骤: 探测点地址的计算:该函数主要用来指定位置注册探测点,首先使用kprobe_addr计算需要插入探测点的地址,这个会设置到kprobe的addr成员,注册后通过这个成员和offset就可以拿到探测位置的地址。利用这个特性,你可以通过kprobe来获取内核中某一个函数的运行时地址 如果没有指定探测地址,而是指定了符号信息,则调用kprobe_lookup_name在内核符号表中查找符号对应的地址,在找到对应的符号地址后,加上偏移就得到探测点的实际位置 如果只是指定了探测点的地址,则会将这个地址直接加上偏移返回 检测探测点地址:计算探测点的地址后,接下来就需要检查这个地址是否可以被探测 跟踪点是否已经被 ftrace 跟踪,如果是就返回错误(kprobe 与 ftrace 不能同时跟踪同一个地址) kprobe只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间中,探测点的地址要么在内核影响中(_stext 和 etext之间,如果是在相同启动阶段(sinittext 和_einittext之间),具体实现在kernel_text_address代码中 跟踪点是否在 kprobe 的黑名单中,如果是就返回错误 如果探测点的地址在一个内核模块中,需要增加对该模板的引用,以防止模块提前卸载,如果模块已经开始卸载,此时也是不能注册探测点 保存被跟踪指令的值: 内核通过调用prepare_kprobe函数来保持被跟踪的指令,而 prepare_kprobe() 最终会调用 CPU 架构相关的 arch_prepare_kprobe() 函数来完成任务 注册kprobe:系统中所有的kprobe实例都保存在kprobe_table这个哈希表中 如果调用get_kprobe()能找到一个kprobe实例,说明已经在当前的探测点注册了一个kprobe,这种情况下会调用register_aggr_kprobe()来处理。 如果当前的探测点没有注册过kprobe,则调用arm_kprobe将被探测位置的指令保持到kprobe的ainsn成员中,并且被探测位置的第一条指令保存到opcode成员中 对于arch_prepare_kprobe,看指令是否是一些分支等特殊指令,需要特别处理。如果是正常可以probe的指令,调用arch_prepare_ss_slot把探测点的指令备份到slot page里,把下一条指令存入struct arch_probe_insn结构的restore成员里,在post_handler之后恢复执行。 arch_prepare_krpobe无误后把kprobe加入kprobe_table哈希链表。 然后调用arch_arm_kprobe替换探测点指令为BRK64_OPCODE_KPROBES指令。 int __kprobes arch_prepare_kprobe(struct kprobe *p){ unsigned long probe_addr = (unsigned long)p->addr; /* 地址应该为4的整数倍 */ if (probe_addr & 0x3) return -EINVAL; /* copy instruction */ p->opcode = le32_to_cpu(*p->addr); /* 大端小端转换,将地址进行转换成PC能识别的地址 */ /* 检测地址是否在异常代码段中 */ if (search_exception_tables(probe_addr)) return -EINVAL; /* 取出探测点的汇编指令 */ /* decode instruction */ switch (arm_kprobe_decode_insn(p->addr, &p->ainsn)) { case INSN_REJECTED: /* insn not supported */ return -EINVAL; /* 异常处理 */ case INSN_GOOD_NO_SLOT: /* insn need simulation */ p->ainsn.api.insn = NULL; break; case INSN_GOOD: /* instruction uses slot */ p->ainsn.api.insn = get_insn_slot(); if (!p->ainsn.api.insn) return -ENOMEM; break; } /* prepare the instruction */ if (p->ainsn.api.insn) arch_prepare_ss_slot(p); /* 将指令存放到slot中,记录吓一条指令到p->ainsn.api.insn */ else arch_prepare_simulate(p); /* 异常处理,如分支指令特殊处理 */ return 0;} 整个过程如下图所示: 最终会调用arm_kprobe,将指令3替换成一条BRK64异常处理指令,当CPU执行到这个跟踪点的时候,将会触发断点中断,这时候就会走到异常处理函数中,对于x86,这个是一条int 3指令,我们来看看针对ARM64,是如何处理的,其最终会调到arch_arm_kprobe,最终会替换成BRK64_OPCODE_KPROBES指令。 /* arm kprobe: install breakpoint in text */void __kprobes arch_arm_kprobe(struct kprobe *p){ void *addr = p->addr; /* 原地址 */ u32 insn = BRK64_OPCODE_KPROBES; /* 替换后的指令 */ aarch64_insn_patch_text(&addr, &insn, 1);} 2.3 触发kprobe探测和回调 kprobe的触发和处理是通过brk exception和single step单步exception执行的,每次的处理函数中会修改被异常中断的上下文(struct pt_regs)的指令寄存器,实现执行流的跳转。ARM64对于异常处理的注册在arch/arm64/kernel/debug-monitors.c, 是arm64的通用debug模块,kgdb也基于这个模块。 void __init debug_traps_init(void){ /* 单步异常处理 */ hook_debug_fault_code(DBG_ESR_EVT_HWSS, single_step_handler, SIGTRAP, TRAP_TRACE, "single-step handler"); /* 断点异常处理 */ hook_debug_fault_code(DBG_ESR_EVT_BRK, brk_handler, SIGTRAP, TRAP_BRKPT, "BRK handler");} 通过hook_debug_fault_code动态定义了异常处理的钩子函数brk_handler,它将在断点异常处理函数中被调用 hook_debug_fault_code是替换arch/arm64/mm/fault.c 中的debug_fault_info异常表项: 对于ARM64的异常处理,当brk断点异常触发后悔执行不同的回调处理,进入异常会跳转到arch/arm64/kernel/entry.S的sync异常处理,此处会跳转到el1_sync 将 entry_handler 1, t, 64, sync宏展开得到调用el1t_64_sync_handler的处理函数,在arch/arm64/kernel/entry-common.c中处理,是通过read_sysreg(esr_el1)来处理对应的异常 最终会调用do_debug_exception处理debug异常 sr_el1的bit27~bit29指示了debug异常类型,对应debug_fault_info数组的索引,此处可知debug异常类型为0x6,对应DBG_ESR_EVT_BRK,由初始化函数debug_traps_init可知inf->fn为brk_handler brk_handler会调用call_break_hook,它实际是根据具体的某种断点异常类型来回调不同的hook,主要是根据ESR_EL1.ISS.Comment进行区分,也就是不同的ESR_EL1.ISS.Comment对应不同的hook。 在初始化时register_kernel_break_hook会向kernel_break_hook链表注册不同的hook,这包括kprobes_break_hook和kprobes_break_ss_hook。list_for_each_entry_rcu(hook, list, node)主要通过遍历kernel_break_hook链表,根据debug断点异常类型找到匹配的hook。 可以看出kprobe_handler里先是进入pre_handler,然后通过setup_singlestep设置single-step相关寄存器,为下一步执行原指令时发生single-step异常做准备 2.4 单步执行 进入异常态后,首先执行pre_handler,然后利用CPU提供的单步调试(single-step)功能,设置好相应的寄存器,将下一条指令设置为插入点处本来的指令,从异常态返回 这个里面使用reenter检查机制,对于SMP,中断等可能有kprobe的重入,允许kpobe发生嵌套 setup_singlestep() 执行完毕后,程序继续执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,单步执行探测点的指令后,会触发单步异常,进入single_step_handler,调用kprobe_breakpoint_ss_handler,主要任务是恢复执行路径,调用用户注册的post_handler kprobe的实现原理是把指定地址(探测点)的指令替换成一个可以让cpu进入debug模式的指令,使执行路径暂停,跳转到probe 处理函数后收集、修改信息,再跳转回来继续执行。 X86中使用的是int3指令,ARM64中使用的是BRK指令进入debug monitor模式。 3 kprobe event实现原理 首先我们跟function一样,从我们的配置开始,krpobe event和功能一样,那么大部分的实现是一样的,最关键的不同就是怎么使用新的插桩方法来创建event。使用向“/sys/kernel/debug/tracing/kprobe_events”文件中echo命令的形式来创建krpobe event。来查看具体的代码实现: 经过层层调用,最终到__trace_kprobe_create函数,其主要的实现如下: 对于alloc_trace_kproe,可以看到kretprobe模式下的桩函数:kretprobe_dispatcher(),而kprobe模式下的插桩函数为kprobe_dispatcher 其最终也会通过__register_trace_kprobe注册kprobe和kpretprobe,其最终的原理也是基本类似 4 kprobe的使用方法 最早的时候,使用kprobe一般都是编写内核驱动,在模块中定义pre-handler和post-handler函数,然后调用kprobe的API(register_kprobe)来进行注册kprobe。加载模块后,pre-handler和post-handler中的printk就会打印定制的信息到系统日志中,目前有三种使用kporbe的接口 kprobe API:使用register_kprobe 基于Ftrace的/sys/kernel/debug/tracing/kprobe_events接口,通过写特定的配置文件 perf_event_open:通过perf工具,perf 的probe命令提供了添加动态探测点的功能, 参看 kernel/tools/perf/Documentation/perf-probe.txt, 在最新的内核上,BPF tracing也是通过这种方式,后面再学习这种方法 kprobes的最大使用者都是一些tracer的前端工具,比如perf、systemtap、BPF 跟踪(BCC和bpftrace) 由于第一种方式灵活而且功能更为强大,对于方法一,大家请参考示例 kprobe:请参考samples/kprobes/kprobe_example.c kretprobe:请参考sample/kprobe/kretprobe_example.c 要编写一个 kprobe 内核模块,可以按照以下步骤完成: 第一步:根据需要来编写探测函数,如 pre_handler 和 post_handler 回调函数。 第二步:定义 struct kprobe 结构并且填充其各个字段,如要探测的内核函数名和各个探测回调函数。 第三步:通过调用 register_kprobe 函数注册一个探测点。 第四步:编写 Makefile 文件。 第五步:编译并安装内核模块。 对于方式二,用户通过/sys/kernel/debug/tracing/目录下的trace等属性文件来探测用户指定的函数,用户可添加kprobe支持的任意函数并设置探测格式与过滤条件,无需再编写内核模块,使用更为简便,但需要内核的debugfs和ftrace功能的支持,详细的请参考内核文档kprobetrace 使用前确定内核CONFIG打开:CONFIG_KPROBE_EVENT=y /sys/kernel/tracing/kprobe_events:添加断点接口 /sys/kernel/tracing/events/kprobes/enabled:断点使能开关 /sys/kernel/tracing/trace:查看trace日志接口 4.1 查看"vfs_open"当前打开文件名 如果你使用了“‘p:’ or ‘r:’+event name” > kprobe_events命令,新的kprobe event将会被添加,可以看到新events对应的文件夹tracing/events/kprobes/,包含‘id’, ‘enabled’, ‘format’ and ‘filter’文件。 enable:使能 filter:过滤想要的信息 trigger:事件发生时触发其他功能,例如function功能 format:环形队列缓冲区的格式 id: event对应的id echo 1 > /sys/kernel/tracing/events/kprobes/myprobe/enable echo 1 > /sys/kernel/tracing/tracing_on 要查看哪些进程触发了这些kprobe,可以通过trace、trace_pipe接口查看,输出格式如下,最左边是进程名,如果是<…>,可能是因为cat的时候,那个进程号对应的进程已经不存在了,第二个是进程PID,触发kprobe的时候记录的。FUNCTION就是触发的那个kprobe的名字,后面括号里是触发的时候代码位置,如果是“r”类型的kprobe,会显示返回到了什么代码位置。代码位置中的行号是反汇编对应的行号。 # 设置kprobe规则,获取vfs_open函数第一个参数path中的文件name cd /sys/kernel/tracingecho 'p vfs_open name=+0x38(+0x8($arg1)):string namep=+0(+0x28(+0x8($arg1))):string' > ./kprobe_events # 使能上述的kprobe echo 1 > ./events/kprobes/p_vfs_open_0/enable # 使能数据写入到 Ring 缓冲区 echo 1 > tracing_on 通过offset和类型打印,实现结构体内部成员的打印,但是需要知道寄存器和参数的对应关系和结构体成员的偏移。 https://www.notion.so/Kprobe-on-ARM64-d6d43e398a5e42b48e752f0f06aa0053#27fdb6f9b3c94d398f9220b495e04107 提到了新的function_event机制,可以直接传递参数名。例如我们想获取net_device的stats信息,获取数据结构偏移的例子:打印ip_rcv的网络设备名和收发包数 $ aarch64-linux-gnu-gdb vmlinux (gdb) ptype/o struct net_device gdb) print (int)&((struct net_device *)0)->stats$7 = 296 cd /sys/kernel/debug/tracing/echo 'p:net ip_rcv name=+0(%x1):string rx_pkts=+296(%x1):u64 tx_pkts=+280(%x1):u64 ' > kprobe_eventsecho 1 > events/kprobes/enable 4.2 设置了一个kretprobe,用来记录返回值 root@rlk:/sys/kernel/tracing# echo 0 > tracing_onroot@rlk:/sys/kernel/tracing# echo 0 > ./events/kprobes/p_vfs_open_0/enableroot@rlk:/sys/kernel/tracing# echo 'p vfs_open name=+0x38(+0x8($arg1)):string namep=+0(+0x28(+0x8($arg1))):string' > ./kprobe_eventsroot@rlk:/sys/kernel/tracing# echo 'r vfs_open ret_val=$retval' >> kprobe_eventsroot@rlk:/sys/kernel/tracing# echo 1 > events/kprobes/p_vfs_open_0/enableroot@rlk:/sys/kernel/tracing# echo 1 > events/kprobes/r_vfs_open_0/enableroot@rlk:/sys/kernel/tracing# echo 1 > tracing_onroot@rlk:/sys/kernel/tracing# echo 0 > tracing_onroot@rlk:/sys/kernel/tracing# cat trace_pipe 4.3 filter:捕获"vfs_open"查看指定文件的信息的事件 # 设置过滤条件,name中包含test字段 echo 'name ~ "*test*"' > ./events/kprobes/p_vfs_open_0/filter 4.4 trigger:包含"test"字段的文件的事件会触发"stacktrace"堆栈打印 # 包含"test"字段的文件的事件会触发"stacktrace"堆栈打印echo 'stacktrace if name ~ "*test*"' > ./events/kprobes/p_vfs_open_0/trigger 5 总结 至此,我们知道Kprobe实现的本质是breakpoint和single-step的结合,这一点和大多数调试工具一样,比如kgdb/gdb。实现动态内核的注入,其主要流程如下: 当 kprobe 被注册后,内核会将对应地址的指令进行拷贝并替换为断点指令(比如 X86 中的 int 3) 随后当内核执行到对应地址时,中断会被触发从而执行流程会被重定向到我们注册的 pre_handler 函数 当对应地址的原始指令执行完后,内核会再次执行 post_handler从而实现指令级别的内核动态监控。也就是说,kprobe 不仅可以跟踪任意带有符号的内核函数,也可以跟踪函数中间的任意指令。

    2024-12-20 224浏览
  • 详解嵌入式屏幕能够显示汉字的原理

    LCD是嵌入式常见设备,如何在LCD上显示汉字和英文?矢量字体和点阵字体有何不同?同一个字符为何有多种编码? GB2312、GB18030指什么?他们之间有关系吗?嵌入式设备如何支持多国语言?从哪里获取字库?需要付费吗? 以上问题,本文一一道来! 一、如何在LCD上显示字符 在LCD上如何显示字符呢?这里我们讨论的是逻辑问题,不是LCD控制器驱动。 首先,我们要对LCD有一个概念:LCD也就是一个点一个点组成的一片点而已。本质上和1个LED一样。 1个单色LED,能显示亮灭。 将很对LED排成一行,就可以实现流水灯。 将多行LED组成一片,是啥?是LED点阵。 多块点阵,就能组成图文屏。 如果是三色LED组成的图文屏,就能显示视频动画。 无论是COG LCD,OLED LCD,还是TFT LCD,实际上都是一个点一个点组成的矩阵,和LED组成的图文屏一样。 当然,制造工艺不一样,控制显示内容技术也不一样,我们不讨论制造技术,控制技术后续会有单独课程讨论。 显然,要在图文屏上显示字符,只需将对应的LED点点亮。不同的字符应该点亮那些LED呢? 我们先逆向想,下图LED点阵上显示的汉字,如何将位置信息保存到代码中,以便下次使用? 这是一个16x16的LED点阵。 每个LED的状态仅仅是亮或灭,那么就可以使用1个bit表示其状态,1表示亮,0表示灭。 1个byte,8个bit,两个byte就可以表示一行LED的状态。 上图这个德字第一行就可以这样表示:0001 0000 0100 0000;也就是0x10,0x40; 按照此方法取得“德”字得完整显示信息: 0001 0000 0100 0000 0x10 0x40 0001 0000 0100 0000 0x10 0x40 0010 1111 1111 1110 0x2F 0xFE 0100 0000 0100 0000 0x40 0x40 1001 0111 1111 1100 0x97 0xFC 0001 0100 1010 0100 0x14 0xA4 0010 0100 1010 0100 0x24 0xA4 0110 0111 1111 1100 0x67 0xFC 1010 0000 0000 0000 0xA0 0x00 0010 1111 1111 1110 0x2F 0xFE 0010 0000 0100 0000 0x20 0x40 0010 0000 0010 0100 0x20 0x24 0010 0101 0010 0010 0x25 0x22 0010 0101 0000 1010 0x25 0x05 0010 1001 0000 1000 0x29 0x08 0010 0000 1111 1000 0x20 0xF8 用1个数组保存,如下: de_dot[32]={ 0x10, 0x40, 0x10, 0x40,0x2f, 0xfe, 0x40, 0x40, 0x97, 0xfc, 0x14, 0xa4,0x24, 0xa4,0x67, 0xfc, 0xa0, 0x00,0x2f, 0xfe,0x20, 0x40,0x20, 0x24, 0x25, 0x22, 0x25, 0x05, 0x29, 0x08, 0x20, 0xf8}; 得到了这个数组,在代码中按照取模的方式将其还原到LED点阵上,就能显示德字。 玩过8段数码管的朋友应该很熟悉,为了在数码管上显示数字,我们会在代码中定义0~9数字的显示掩码(数码管每个段亮灭的信息)。 点阵上显示汉字,无非就是LED更多了。 因此,要在LCD上显示字符,需要先知道如何描绘字符的信息,按照取模方式,将这写信息还原到LCD上,就能描绘出字符。 二、点阵字库 那什么是点阵字库呢? 上面说的“德”字的描绘信息数组就是点阵字库。 这个德字描绘出的效果,就是点阵字体。描绘信息保存的形式,就是点阵字库。 点阵字库有多种形式: 1、直接将信息用数组保存到代码中。(显示的内容较少时通常会这样做) 2、将点阵信息描绘到一张bmp图片,使用时根据信息文件(FNT)从图片中取信息。(很多游戏使用的就是这种贴图字体,也叫bmpfont) 3、将一堆点阵信息数组打包为1个bin文件,使用算法定位字符位置。(点阵较多,按照编码规范整合在一起) 4、按照电脑字体规范整合的字体。(比如windows的ttf字体,bdf字体) 三、取模方式 取模方式指的是单个bit位置信息组合成字节时使用的方式。 前面我们对德字取位置信息时:使用横向取模,并且高位在前。此外还有很多不同的取模方式,常见的方式如下: 尺寸 汉字通常有这些尺寸:12x12,16x16,24x24。在cog屏128*64像素的屏上,一般用12x12的汉字点阵,可以显示5行。 ASCII码的点阵通常和汉字高度一样,宽度是一半,比如,12x12的汉字配12x6的ASCII码,16x16的汉字配16x8的ASCII码。 四、矢量字体 矢量字体:矢量字体(Vector font)中每一个字形是通过数学曲线来描述的,它包含了字形边界上的关键点,连线的导数信息等,字体的渲染引擎通过读取这些数学矢量,然后进行一定的数学运算来进行渲染。 这类字体的优点是字体实际尺寸可以任意缩放而不变形、变色。矢量字体主要包括 Type1 、 TrueType、OpenType等几类。 Freetype:FreeType库是一个完全免费(开源)的、高质量的且可移植的字体引擎,它提供统一的接口来访问多种字体格式文件,包括TrueType, OpenType, Type1, CID, CFF, Windows FON/FNT, X11 PCF等。 矢量字体使用Freetype进行渲染后,最后得到的也是bitmap,毕竟,LCD就是一个一个点组成的。 理论上矢量字体可以无限放大而不失真。 但是,矢量字体渲染为较小字号的字,某些字(笔画多)可能失真非常严重。 五、字符编码 字符编码是指一种映射规则,根据这个映射规则可以将某个字符映射成其他形式的数据以便在计算机中存储和传输。 ASCII码 ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。 它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符 。 Codepage 欧美很多国家的语言在ASCII码中没有定义,各个国家(或组织)就使用1个字节剩下的127个值映射他们需要的字符。 国家很多,定义很多,如何决定使用哪种映射呢? IBM、微软等系统就增加了CodePage的概念:每种映射分配一个编号。这些系统的代码页编号是不完全一样的。IBM定义的代码叫做OEM,微软定义的代码叫ANSI。 六、汉字编码 汉字字符数量很多,仅仅使用高位127个值不能表示。 因此国家标准组织定义了《信息交换用汉字编码字符集》。有三个版本: GB2312编码:1981年5月1日发布的简体中文汉字编码国家标准。GB2312对汉字采用双字节编码,收录7445个图形字符,其中包括6763个汉字。 GBK编码:1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,对汉字采用双字节编码。GBK字符集共收录21003个汉字,包含国家标准GB13000-1中的全部中日韩汉字,和BIG5编码中的所有汉字。 GB18030编码:2000年3月17日发布的汉字编码国家标准,是对GBK编码的扩充,覆盖中文、日文、朝鲜语和中国少数民族文字,其中收录27484个汉字。GB18030字符集采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。 汉字编码使用分区概念,如下面分区,其中的双字节二区就是符合GB2312标准的双字节区。 双字节部分编码空间结构图 七、如何获得字库 在介绍如何获得字库之前,先说明版权问题 版权说的是字体,也就是一个字符描绘出来的效果。一个字体的形成方式通常是矢量字体或者点阵字库。 使用工具将一种字体的矢量字库转换为点阵字库,他们仍然是同一种字体,那么版权是一样的,依然属于矢量字体拥有者。 获取矢量字体 在你的电脑中就有很多字体。windows下的C:\Windows\Fonts下保存有你电脑安装的字体,这种字体通常是ttf标准。 这些字体一般都不是开源的,也就是不能免费商用到嵌入式设备上。开源的字体有:思源字体,可以免费商用。 八、获取点阵字体 我们更关心的是如何获取点阵字体。 前面说过,点阵字体有很多存在方式,那么我们就有很多获取方式。 从很早之前的电脑汉卡上获取点阵字库。这种方式比较难找,在github上可以找到一些外文的点阵字库,汉字的没找到。 从DOS系统获取汉字点阵这种点阵我用过的有HZ1616,HZ1212。这种字体的版权不是很清楚属于谁。不过这种字库因为很早,所以基本都是GB2312规范的,不包含生僻字。 从电脑字体中找点阵字体电脑字体常见的是TTF规范,这个规范比代表它只有矢量字体,很多电脑字体中会包含矢量字体和点阵字体。点阵字体一般是小号字。小号字用矢量字体渲染,效果不好,所有某种字体通常会带小号的点阵字体。 用取模工具取点阵信息 如果使用的字符很少,可以用《zimo3》等工具取模。 用点阵生成工具批量生成 比如易木雨软件工作室做的《字库制作软件.rar》,可以批量将矢量字库转为点阵字库。 从开源模块中找外文点阵 比如tslib触摸库中就含有英文点阵 买。有一个叫高通的公司,卖点阵字库芯片。有一个较北京中易的公司,有一套很好的点阵字库。这套点阵字库由于性能好,流传广,工作经验丰富的工程师一般都接触过;不过大部分公司可能都没有获得授权。还有其他很多做字体的公司也有点阵字库,比如方正。 自己用编辑工具一个一个画,然后用你的脑子将其转为数组。

    2024-12-19 141浏览
正在努力加载更多...
EE直播间
更多
广告