• 单片机程序结构优化

    1、程序的书写结构 虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该遵循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。 在书写程序时,特别是对于While、for、do…while、if…else、switch…case 等语句或这些语句嵌套组合时,应采用“缩格”的书写形式。 2、标识符 程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:count、number1、red、work 等。 3、程序结构 C 语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用C 语言设计单片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可使整个应用系统程序结构清晰,便于调试和维护。 对于一个较大的应用程序,通常将整个程序按功能分成若干个模块,不同模块完成不同的功能。 各个模块可以分别编写,甚至还可以由不同的程序员编写,一般单个模块完成的功能较为简单,设计和调试也相对容易一些。在C 语言中,一个函数就可以认为是一个模块。 所谓程序模块化,不仅是要将整个程序划分成若干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保持模块的独立性,尽量少使用全局变量等。对于一些常用的功能模块,还可以封装为一个应用程序库,以便需要时可以直接调用。 但是在使用模块化时,如果将模块分成太细太小,又会导致程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。 4、定义常数 在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。 5、减少判断语句 能够使用条件编译(ifdef)的地方就使用条件编译而不使用if 语句,有利于减少编译生成的代码的长度。 6、表达式 对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。 7、函数 对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保证它与原来定义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。如果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数。 如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报错,这样会增加排错的难度。 8、尽量少用全局变量,多用局部变量 因为全局变量是放在数据存储器中,定义一个全局变量,MCU 就少一个可以利用的数据存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配;而局部变量大多定位于MCU 内部的寄存器中,在绝大多数MCU 中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所能占用的寄存器和数据存储器在不同的模块中可以重复利用。 9、设定合适的编译程序选项 许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最合适的一种优化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,可能会影响程序的正确性,导致程序运行出错。 因此应熟悉所使用的编译器,应知道哪些参数在优化时会受到影响,哪些参数不会受到影响。 代码的优化 1、选择合适的算法和数据结构 应熟悉算法语言。将比较慢的顺序查找法用较快的二分查找法或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,这样可以大大提高程序执行的效率。 选择一种合适的数据结构也很重要,比如在一堆随机存放的数据中使用了大量的插入和删除指令,比使用链表要快得多。数组与指针具有十分密切的关系,一般来说指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。 但是在Keil 中则相反,使用数组比使用的指针生成的代码更短。 2、使用尽量小的数据类型 能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。 当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。 3、使用自加、自减指令 通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1之类的指令,有很多C 编译器都会生成2~3个字节的指令。 4、减少运算的强度 可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下: (1)求余运算 a=a%8; 可以改为: a=a&7; 说明:位操作只需一个指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n 方的余数,均可使用位操作的方法来代替。 (2)平方运算 a=pow(a,2.0); 可以改为: a=a*a; 说明:在有内置硬件乘法器的单片机中(如51 系列),乘法运算比求平方运算快得多,因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR 单片机中,如ATMega163 中,乘法运算只需2 个时钟周期就可以完成。 即使是在没有内置硬件乘法器的AVR单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。如果是求3 次方,如: a=pow(a,3.0); 更改为: a=a*a*a; 则效率的改善更明显。 (3)用移位实现乘除法运算 a=a*4; b=b/4; 可以改为: a=a<<2; b=b>>2; 说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR 中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。 用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如: a=a*9 可以改为: a=(a<<3)+a 5、循环 (1)循环语对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init 的初始化程序中进行。 (2)延时函数 通常使用的延时函数均采用自加的形式: void delay (void){unsigned int i;for (i=0;i<1000;i++); }将其改为自减延时函数:void delay (void){unsigned int i;for (i=1000;i>0;i--); } 两个函数的延时效果相似,但几乎所有的C 编译对后一种函数生成的代码均比前一种代码少1~3 个字节,因为几乎所有的MCU 均有为0转移的指令,采用后一种方式能够生成这类指令。在使用while 循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3 个字母。 但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。 (3)while 循环和do…while 循环 用while 循环时有以下两种循环形式: unsigned int i;i=0;while (i<1000){i++; //用户程序}或:unsigned int i;i=1000;do{i--; //用户程序}while (i>0); 在这两种循环中,使用do…while循环编译后生成的代码的长度短于while循环。 6、查表 在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。 如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,后面在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。 7、其它 比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化。 乘除法优化 目前单片机的市场竞争很激烈,许多应用出于性价比的考虑,选择使用程序存储空间较小(如1K,2K)的小资源8位MCU芯片进行开发。一般情况下,这类MCU没有硬件乘法、除法指令,在程序必须使用乘除法运算时,如果单纯依靠编译器调用内部函数库来实现,常常会有代码量偏大、执行效率偏低的缺点。 上海晟矽微电子推出的MC30、MC32系列MCU,采用了RISC架构,在小资源8位MCU领域有广大的用户群和广泛的应用,本文就以晟矽微电的这两个系列产品的指令集为例,结合汇编与C编译平台,给大家介绍一种既省时又节约资源的乘除法算法。 1、乘法篇 单片机中的乘法是二进制的乘法,也就是把乘数的各个位与被乘数相乘,然后再相加得出,因为乘数和被乘数都是二进制,所以实际编程时每一步的乘法可以用移位实现。 例如:乘数R3=01101101,被乘数R4=11000101,乘积R1R0。步骤如下: 1、清空乘积R1R0; 2、乘数的第0位是1,那被乘数R4需要乘上二进制数1,也就是左移0位,加到R1R0里; 3、乘数的第1位是0,忽略; 4、乘数的第2位是1,那被乘数R4需要乘上二进制数100,也就是左移2位,加到R1R0里; 5、乘数的第3位是1,那被乘数R4需要乘上二进制数1000,也就是左移3位,加到R1R0里; 6、乘数的第4位是0,忽略; 7、乘数的第5位是1,那被乘数R4需要乘上二进制数100000,也就是左移5位,加到R1R0里; 8、乘数的第6位是1,那被乘数R4需要乘上二进制数1000000,也就是左移6位,加到R1R0里; 9、乘数的第7位是0,忽略; 10、这时候R1R0里的值就是最后的乘积,至此算法完成。 以上例子运算结果: R1R0 = R3 * R4= (R4<<6)+(R4<<5)+(R4<<3)+(R4<<2)+R4 = 101001111100001 实际运算流程图见下图: 在实际的程序设计过程中,程序优化有两个目标,提高程序运行效率,和减少代码量。我们来看下本文提供的汇编算法和普通C语言编程的效率和代码量对比。 表1.1是程序运行效率的对比数据(可能会有小的偏差),很明显汇编编译出来的运行时间要比C语言减少很多。 汇编(时钟周期) C语言(时钟周期) 8*8位乘法 79-87 184-190 16*8位乘法 201-210 362-388 16*16位乘法 234-379 396-468 表1.1  乘法运算时钟周期对比表 表1.2是程序代码量的对比数据(可能会有小的偏差),汇编占用的程序空间也要比C语言小很多。 汇编(Byte) C语言(Byte) 8*8位乘法 15 34 16*8位乘法 19 96 16*16位乘法 31 96 表1.2  乘法运算ROM空间使用情况对比表 综上两点,本文介绍的乘法算法各方面使用情况都要比C编译好很多。如果大家在使用过程中,原有的程序不能满足应用需求,例如遇到程序空间不够或者运行时间太久等问题,都可以按照以上方式进行优化。 汇编语言最接近机器语言的。在汇编语言中可以直接操作寄存器,调整指令执行顺序。由于汇编语言直接面对硬件平台,而不同的硬件平台的指令集及指令周期均有较大差异,这样会对程序的移植和维护造成一定的不便,所以我们针对精简指令集做了乘法运算的例程,便于大家的移植和理解。 2、除法篇 单片机中的除法也是二进制的除法,和现实中数学的除法类似,是从被除数的高位开始,按位对除数进行相除取余的运算,得出的余数再和之后的被除数一起再进行新的相除取余的运算,直到除不尽为止,因为单片机中的除法是二进制的,每个步骤除出来的商最大只有1,所以我们实际编程时可以把每一步的除法看作减法运算。 例如:被除数R3R4=1100110001101101,除数R5=11000101,商R1R0,余数R2。步骤如下: 1、清空商R1R0,余数R2;2、被除数放开最高位,第15位,为1,1比除数小,商为0,余数R2为1;3、上一步余数并上被除数次高位,第14位,得11,11仍然比除数小,商为0,余数R2为114、直到放开第8位后,得11001100,比除数大,商得1,余数R2为111;5、上一步余数并上被除数第7位,得1110,没有除数大,商为0,余数R2为1110;6、上一步余数并上被除数第6位,得11101,没有除数大,商为0,余数R2为11101;7、按照以上步骤,直到放开了被除数得第3位,得11101101,比除数大,商为1,余数R2为101000;8、上一步余数并上被除数第2位,得1010001,没有除数大,商为0,余数R2为1010001;9、上一步余数并上被除数第1位,得10100010,没有除数大,商为0,余数R2为10100010;10、上一步余数并上被除数第0位,得101000101,比除数大,商为1,余数R2为10000000;11、然后把以上所有步骤中得商从左至右依次排列就是最后的商100001001,余数为最后算得的余数10000000。 以上例子运算结果:R1R0 = R3R4 / R5 = 100001001 ;R2 = R3R4 % R5 = 10000000 实际运算流程图见下图: 除法运算的效率,代码量见以下表格 表2.1是程序运行效率和代码量的对比数据(可能会有小的偏差),很明显本文提供的汇编算法要优化的很多。 16/8位除法 汇编 C语言 时钟周期 287-321 740-804 使用空间(Byte) 35 142 表2.1  除法运算时钟周期对比表 所以对于除法运算,本文提供的方法也是相对较优的。 以下是针对精简指令集做的除法运算,16/8位的例程,便于大家的移植和理解。

    03-14 137浏览
  • 一文理解单片机BootLoader的前世今生

    Bootloader是一个用于引导嵌入式系统的软件程序,通常存储在芯片的非易失性存储器中,如闪存。Bootloader的主要功能是在系统上电后,负责初始化系统硬件并加载操作系统或应用程序到内存中执行。 下面是一个简要的介绍Bootloader的工作流程和主要功能: 硬件初始化:Bootloader首先负责初始化处理器、时钟、内存和外设等硬件资源,确保系统处于合适的状态以便后续的操作。 引导设备选择:Bootloader可能需要检测可用的引导设备,例如闪存、外部存储器或串口等,并选择其中一个作为加载操作系统或应用程序的来源。 加载操作系统或应用程序:Bootloader根据预定义的引导策略,从选定的引导设备中读取操作系统镜像或应用程序,并将其加载到内存中。 执行加载的程序:一旦程序加载到内存中,Bootloader会跳转到该程序的入口点,启动执行。对于操作系统,这意味着将控制权移交给操作系统内核,而对于应用程序,它将开始执行应用程序的主函数。 错误处理和回滚:Bootloader通常包含错误处理机制,以便在发生错误时执行相应的操作,例如跳转到备用引导设备或者恢复到出厂设置状态。 升级支持:一些Bootloader还提供了固件升级的支持,允许通过特定的接口或协议更新系统固件,而无需物理访问设备。 总的来说,Bootloader在嵌入式系统中扮演着关键的角色,负责启动和管理系统的引导过程,同时提供了灵活性和可靠性,以适应不同的应用需求。 当我面对一个有一定规模、 稍显复杂的嵌入式项目时, 我通常并不会直接专注于主要功能的实现, 而是会做一些磨刀不误砍柴工的工作———设计一个 Bootloader(以下简称 BL) 以及构建一个Shell 框架。可能有人会觉得它们很高深, 实则不难, 正所谓“会者不难, 难者不会”。 本章就针对 BL 进行详细的讲解, 希望让大家可以体会到它的重要性。 1. 烧录方式的更新迭代 1.1 古老的烧录方式 单片机诞生于20 世纪80 年代, 以51 为代表开始广泛应用于工业控制、 家电等很多行业中。起初对于单片机的烧录, 也就是将可执行的程序写入到其内部的 ROM 中, 这不是一件容易的事情, 而且成本不低, 因为需要依赖于专门的烧录设备。而且受到半导体技术与工艺的限制, 对于 ROM 的烧写大多需要高压。这种境况一直持续到2000 年左右( 我上大学的时候还曾用过这种专门的烧录器),: 1.2 ISP 与ICP 烧录方式 随着低压电可擦写 ROM 的成熟, 单片机开始集成可通过数字电平直接读写的存储介质。其最大的优势在于可实现在系统或在电路直接烧录程序, 而无须像以前一样把单片机芯片从电路中拿出来, 放到编程器上, 这种烧录方式就是ISP(In System Programming) 或ICP (In Circuit Programming), 如图7.2 所示。 有人问过这样一个问题:“ISP 和ICP 我都听说过, 都说是可以在电路板上直接烧录程序,而无须拿下芯片, 那ISP 和ICP 有什么区别?”从广义上来说, 两者没有区别, 平时我们把其意义混淆也毫无问题。非要刨根问底的话, 那可以这样来理解:ISP 要求单片机中驻留有专门的程序, 用以与上位机进行通信, 接收固件数据并烧录到自身的 ROM 中, 很显然ISP 的单片机是需要可运行的, 即要具备基本的最小系统电路(时钟和复位); 而ICP 可以理解为 MCU 就是一块可供外部读写的存储电路, 它不需要预置任何程序, 也不需要单片机芯片处于可运行的状态。 支持ISP 或ICP 的芯片, 以 AT89S51 最为经典, 当时从 AT89C51 换成 S51 , 多少人曾因此不再依赖烧录器而大呼爽哉。这种并口下载线非常流行, 如图7.3 所示, 网上还有各种ISP小软件, 可以说它降低了很多人入门单片机的门槛, 让单片机变得喜闻乐见。一台电脑、一个S51 最小系统板、 一条并口ISP 下载线, 齐了! 1.3 更方便的ISP 烧录方式 1. 串口ISP 但是后来我们发现带有并口的电脑越来越少。那是在2005 年前后,STC 单片机开始大量出现, 在功能上其实与S51 相差无几, 甚至比同期的一些高端51 单片机还要逊色。但是它凭借一个优势让人们对它爱不释手, 进一步降低了单片机的学习门槛。这个优势就是———串口ISP, 这是真正意义上的ISP, 如图7.4 和图7.5 所示。再后来,9 针串口都很少见了, 只有 USB。这促使一个烧录和调试神器炙手可热———USB-TTL 串口。这下232 转换芯片省掉了, 直接通过 USB 进行烧录。这种方式造福了无数的单片机学习者和工程师。我本人虽然已经搞了近20 年单片机和嵌入式, USB 串口依然是不可或缺的调试工具。 多年来, 在串口与单片机的交互上, 我动了很多脑筋, 这也是我乐于开发 Bootloader 的一个原因。我希望“USB 串口在手, 一切全有!” STC 并不是第一个使用串口ISP 烧录程序的, 但它是最成功和最深入人心的。与之同期的很多单片机, 包括时至今日仍然应用最广泛的 STM32 全系列也都支持了串口ISP, 它成了一种标配的、 非常普遍的程序烧录手段。 2. 各种 USBISP 串口ISP 固然方便, 但是下载速度是它的硬伤, 当固件体积比较大的时候, 比如一些大型嵌入式项目的固件动辄几百 KB, 甚至几 MB, 再用串口ISP 就未免太慢了。所以一些单片机配有专门的 USBISP 下载器。以下列举几种比较主流的单片机及其 USBISP 下载器。 (1) AVR AVR 单片机曾经盛极一时, 但经历了2016 年的缺芯风波之后, 加之 STM32 的冲击, 开始变得一蹶不振, 鲜有人用了。与之配套的 USBISP 下载器非常多样, 有些是官方发布的, 更多的是爱好者开源项目的成果, 如图7.6 所示。 (2) C8051F( 见图7.7) (3) MSP430( 见图7.8) 我们会发现, 一个具有良好生态的主流单片机, 一定有配套的高效便捷的烧录下载工具。可见一种好的烧录方式, 对单片机开发是多么重要。 不论是串口ISP 还是各种专用的ISP 下载器, 都有一些共同的弊端。 依赖于专门的上位机或下载器硬件, 不能做到统型; 下载器价格仍然比较高, 尤其是原厂的, 这也是为什么有些单片机催生出很多第三方的下载器, 比如 AVR; 下载的时候通常需要附加额外的操作, 比如 STC 要重新上电、STM32 需要设置BOOT 引脚电平等。 这些额外的操作都增加了烧录的复杂性。尤其是在产品形态下要去重新烧录程序, 比如嵌入式升级, 就要打开外壳, 或将附加信号引出到壳外。 这都是非常不高效, 不友好的做法。如果有一种烧录方法, 对于任何一种单片机: 通信方式统一(比如一律都用串口); 提供一个友好的操作界面(比如命令行方式); 高效快速, 没有附加操作, 最好一键自动化烧录; 另外再增加一些嵌入式固件管理的功能(比如固件版本管理)。 这一定会让我们事半功倍。Bootloader 就能实现上述的这一切! 2. 关于Bootloader 2.1 Bootloader 的基本形态 直接看图7.9: 可以看到 BL 就是一段存储在 ROM 中的程序, 它主要实现4 个功能: 通过某种途径获取要烧录的固件数据; 将固件数据写入到 ROM 的 APP 区中; 跳转到 APP 区运行, 将烧录进去的用户程序引导起来; 在此过程中, 提供必要而友好的人机交互界面。这么说可能不好理解, 我们还是通过实例来进行讲解。 2.2 Bootloader 的两个设计实例 下面的两个实例, 用于说明 BL 的实际应用形态, 不涉及具体的实现细节, 旨在让大家了解 BL 实际是如何运行的。 1. 带Shell 命令行的串口BL 基本的操作逻辑如下: 通过超级终端、SecureCRT 或 Xshell 之类的串口终端输入命令program; BL 接收到命令后, 开始等待接收固件文件数据; 串口终端通过某种文件数据传输协议将固件数据传给 BL; BL 将固件数据写入到 ROM 的 APP 区中; BL 将 APP 区中的程序引导运行起来。更具体的示意如图7.10 所示。这里把操作逻辑说得很简单, 实际实现起来却并不容易, 我们放在后面去细究其具体实现。 2. 插SD 卡即烧录的BL 基本的操作逻辑如下: 将待烧录的固件拷贝到SD 卡中; 将SD 卡插入到卡槽中; BL 检测到SD 卡插入, 搜索卡中 BIN 文件; 将 BIN 文件数据读出写入到 ROM 的 APP 区中; BL 将 APP 区中的程序引导运行起来。 如图7.11 所示。通过这两个设计实例, 大家应该已经了解BL 是什么了吧。有没有感受到BL 是比ISP 烧录器更通用、 更灵活、 更友好、 功能更强大的固件烧录和管理手段呢? 有人可能知道 Linux 下的 Uboot, 它就是一个强大的 BL, 它提供非常强大的刷机(烧录操作系统镜像) 的功能以及完备而灵活的Shell 界面, 如图7.12 所示。其实我们电脑的 BIOS 也是一种广义的 BL。 那如何实现一个 BL 呢? 别急, 要实现 BL 是需要满足一些基本要求的。 2.3 BL 实现的要点 首先要说, 并不是任何一个单片机都可以实现 BL 的, 要满足几个要点。 1. 芯片体系架构要支持 来看图7.13 。 我们知道单片机程序的最开头是中断向量表, 包含了程序栈顶地址以及 Reset 程序入口,通过它才能把程序运行起来。很显然在从 BL 向 APP 跳转的时候,APP 程序必须有自己的中断向量表。而且单片机体系架构上要允许中断向量表的重定向。 传统51 单片机的中断向量表只允许放到 ROM 开头, 而不能有偏移量, 所以传统51单片机是不能支持 BL 的。有人要问“你这不是自相矛盾吗? 你前面说 STC 的51 单片机是支持串口ISP 的, 那它应该内置有ISP 程序, 我理解它应该和BL 是一个道理。”没错, 它内置的ISP程序就是一种 BL。STC 之所以可以实现 BL 功能, 是因为宏晶半导体公司对它的硬件架构进行了改进, 请看图7.14 可以看到,STC51 单片机多出了一块专门存放 BL 的 ROM, 称为 BOOTROM。 网上有一位叫 shaoziyang 的网友为 AVR 单片机写了一个 BL, 还配套开发了 一 款 叫AVRUBD 的上位机, 如图7.15 (AVRUBD 是很有用的, 本章后面会介绍, 它可以让我们实现隔空烧录) 所示, 实现了 AVR 单片机的串口烧录, 让很多人摆脱了对 USBISP 之类ISP 下载器的依赖(虽然ISP 下载器已经很方便了, 但它毕竟还需要银子嘛)。 AVR 在硬件架构上与STC51 是一个套路, 如图7.16 所示。 通过配置 AVR 的熔丝位可以控制复位入口地址以及 BOOT 区的大小和开始地址, 如图7.17 所示。 讲到这里, 有人会说:“那有没有一种单片机, 程序放在 ROM 的任何位置都可以运行起来, 也就是中断向量表可以重定位?”当然有, 这种单片机还很多, 其中最典型的就是 STM32。它的程序之所以可以放之各地皆可运行, 是因为在它的 NVIC 控制器中提供了中断向量表偏移量的相关配置, 这个后面我们再详细说。 2. ROM 要支持IAP 这也是需要单片机硬件支持的。很好理解, 在 BL 获取到固件数据之后, 需要将它写入到ROM 的 APP 区中, 所以说单片机需要支持IAP 操作, 所谓IAP 就是 In Application Programming, 即在应用烧录。也就是在程序运行过程中, 可以对自身 ROM 进行擦除和编程操作 。 大家仔细想想是不是这样? 似乎支持串口ISP 的单片机都支持IAP 功能。STC 还把这一功能包装成了它的一大特色, 可以用内部 ROM 来充当 EEPROM 的功能, 可以在运行时记录一些掉电不丢失的参数信息。 STM32 的 ROM 擦写在配套的固件库(标准库或 HAL 库) 中已经有实现, 大家可以参考或直接使用。 3. APP 程序的配套修改 为了让 BL 可以顺利地将 APP 程序引导运行起来, APP 程序在开发的时候需要配合 BL做出相应的修改。最重要的就是 APP 程序的开始地址(即中断向量表的开始地址) 以及对中断控制器的相应配置。 对于51 、AVR 这类单片机 APP 程序不用修改, 具体原因大家应该明白。这里主要对STM32 APP 程序如何修改进行详细讲解。 我们依然是结合实例, 如图7.18 所示。 假设我们所使用的STM32 的 ROM 总大小为128KB,BL 程序的体积是16KB,APP 程序紧邻 BL, 那么 APP 区的开始地址为0X08004000, 也就是 APP 程序的中断向量表偏移地址为0X4000。 如果我们使用 MDK 作为开发环境的话, 需要修改这里, 如图7.19 所示。 而如果我们使用的是gcc 的话, 则需要对link.ld 链接文件进行修改, 如图7.20 所示。 然后我们还需要对 NVIC 的中断向量表相关参数进行配置, 主要是中断向量表的偏移量,如下代码: #define VECT_TAB_OFFSET 0x4000 OK, 经过修改后的程序, 我们把它放到 ROM 的0X08004000 开始地址上, 然后再让 BL跳转到这个地址, 我们的程序就能运行起来了。 有人又会问:“BL 中的跳转代码怎么写?”别急, 这是我们要讲的下一个要点 4. BL 中的跳转代码 跳转代码是 BL 要点中的关键, 直接关系到 APP 程序能否正常运行, 如图7.21 所示。我直接给出STM32 的 jump _app 函数代码。 typedef void ( * iapfun)(void); iapfun jump2app; void MSR_MSP(u32 addr) { __ASM volatile("MSR MSP, r0" ); //set Main Stack value __ASM volatile("BX r14" ); } void load_app(u32 appxaddr) { if((( * (vu32* )appxaddr)&0x2FFE0000) ==0x20000000) //检查栈顶地址合法 { //用户代码区第二个字为程序开始地址(复位地址) jump2app= (iapfun) * (vu32* )(appxaddr+4); //初始化APP 堆栈指针(用户代码区的第一个字用于存放栈顶地址) MSR_MSP( * (vu32* )appxaddr); jump2app(); //跳转到APP. } } 这段代码大家自行研究, 如果展开讲就属于赘述了。到这里 BL 相关的要点就介绍完了, 大家应该有能力去完成一个简单的 BL 了。我基于STM32 设计了一个小实验, 大家有兴趣可以小试牛刀一下, 如图7.22 所示。 我们将 BL 程序用Jlink 烧录到0X08000000 位置, 而把 APP 程序烧录到0X08002000 开始位置, 然后复位, 如果串口打印了hello world 或流水灯亮起来了, 就说明我们的BL 成功了。 3. 把Bootloader 玩出花 上面我所讲的都是 BL 最基础的一些内容, 是我们实现 BL 所必须了解的。BL 真正的亮点在于多种多样的固件数据获取方式。 3.1 BL 的实现与延伸(串口传输固件) 前面我讲到过两个 BL 应用的实例, 一个是串口传输固件文件, 一个是 SD 卡拷贝固件文件。它们是在实际工程中经常被用到的两种 BL 形式。 这里着重对前一个实例的实现细节进行讲解剖析, 因为它非常具有典型意义, 如图7.23 所示。 这个流程图提出了3 个问题: 串口通信协议是如何实现的? 为什么获取到上位机传来的固件数据,不是直接写入到 APP 区,而是先暂存,还要校验? 对固件数据是如何实现校验的? 串口通信协议以及文件传输实现的相关内容略显繁杂, 在本书《 大话文件传输》一章中会专门进行讲解。 第二个问题: 经过串口传输最终由单片机接收到的固件数据是可能出现差错的, 而有错误的固件冒然直接写入到 APP 区, 是一定运行不起来的。所以, 我们要对数据各帧进行暂存, 等全部传输完成后, 对其进行整体校验, 以保证固件数据的绝对正确。 针对第三个问题, 我们要着重探讨一下。 一个文件从发送方传输到接收方, 如何确定它是否存在错误? 通常的做法在文件中加入校验码, 接收方对数据按照相同的校验码计算方法计算得到校验码, 将之与文件中的校验码进行对比, 一致则说明传输无误, 如图7.24 所示。 图7.24 是对固件文件的补齐以及追加校验码的示意。为什么要对文件补齐? 嵌入式程序经过交叉编译生成的可烧录文件, 比如 BIN, 多数情况下都不是128、256 、512 或1024 的整数倍。这就会导致在传输的时候, 最后一帧数据的长度不足整帧, 就会产生一个数据尾巴。取整补齐是解决数据尾巴最直接的方法。这一操作是在上位机上完成的, 通常是编写一个小软件来实现。这个小软件同时会将校验码追加到固件文件末尾。这个校验码可以使用校验和(CheckSum) 或者 CRC, 一般是16 位或32 位, 如图7.25 所示。 又有人会问:“要把整个固件暂存下来, 再作校验, 那得需要额外的存 储 空 间 吧, 外 扩ROM(FlashROM 或 EEPROM)?”是的。如果想节省成本, 我们也可以不暂存, 传输时直接烧写到 APP 区。这是有风险的, 但是一般来说问题不大(STC 和 STM32 的串口ISP 其实也都是实时烧写, 并不暂存)。因为在传输的过程中, 传输协议对数据的正确性是有一定保障的, 它会对每一帧数据进行校验, 失败的话会有重传, 连续失败可能会直接终止传输。所以说, 一般只要传输能够完成, 基本上数据正确性不会有问题。但是仍然建议对固件进行整体校验, 在成本允许的情况下适当扩大 ROM 容量。同时, 固件暂存还有一个另外的好处, 在 APP 区中的固件受到损坏的时候, 比如固件意外丢失或IAP 时不小心擦除了 APP 区, 此时我们还可以从暂存固件恢复回来(完备的 BL 会包含固件恢复的功能)。 其实也不必非要外扩 ROM, 如果固件体积比较小的话, 我们可以把单片机的片上 ROM砍成两半来用, 用后一半来作固件暂存。 如图7.26 所示, 我们将片上 ROM 划分为3 部分, 分别用于存储 BL、APP 固件以及暂存固件。比如 我 们 使 用 STM32F103RBT6 , 它 一 共 有 128 KB 的 ROM, 可 以 划 分 为 16 KB/ 56 KB/56 KB。 有些 产 品 对 成 本 极 为 敏 感。我 就 有 过 这 样 的 开 发 经 历, 当 时 使 用 的 单 片 机 是STM32F103C8T6 , 片上 ROM 总容量为64 KB, 固件大小为48 KB,BL 为12 KB。在通过BL进行固件烧写时根本没有多余的 ROM 进行固件暂存。我使用了一招“狗尾续貂”, 如图7.27所示。 我无意中了解到 STM32F103C8T6 与 RBT6 的晶元是同一个。只 是 因 为 有 些 芯 片 后64 KB 的 ROM 性能不佳或有瑕疵, 而被限制使用了。我实际测试了一下, 确实如此。但是后64 KB ROM 的使用是有前提的, 也就是需要事先对其好坏进行验证。如果是好的, 则暂存校验, 再写入 APP 区; 而如果是坏的, 那么就直接在固件传输时实时写入 APP 区(这个办法我屡试不爽, 还没有发现后64KB 有坏的)。 以上所介绍的是一种“骚操作”, 根本上还是有一定的风险的,ST 官方有声明过, 对后64K ROM 的质量不作保证, 所以还是要慎用。 3.2 10 米之内隔空烧录 OTA (On the Air) 这个“隔空烧录”源于我的一个IoT 项目, 它是对空调的外机进行工况监测。大家知道,空调外机的安装那可不是一般人能干的, 它要不就在楼顶, 要不就在悬窗上。这给硬件升级嵌入式程序带来很大的困难。所以, 我实现了“隔空烧录”的功能, 其实它就是串口 BL 应用的一个延伸, 如图7.28 所示。 “隔空烧录确实牛, 但是总要抱着一个电脑, 这不太方便吧。”确实是! 还记得前面我提过的 AVRUBD 通信协议吗? (详见“大话文件传输”一章) 它的上位机软件是有手机版的。这样我们只要有手机, 就能“隔空烧录”了, 如图7.29 所示。 “哪个 APP? 快告诉我名字”, 别急, 蓝牙串口助手安卓版, 图 7.30 是正在传输固件的界面。 AVRUBD 其实是对 Xmodem 协议的改进, 这个我们放在专门的章节进行详细讲解。 3.3 BL 的分散烧录 我们知道 BL 的核心功能其实就是程序烧录。那你有没有遇到过比较复杂的情况, 如图7.31 所示。 这种情况是有可能遇到的。主 MCU+CPLD+ 通信协处理器+ 采集协处理器就是典型的复杂系统架构。这种产品在批量生产阶段, 烧录程序是非常烦琐的。首先需要维护多个固件, 再就是需要一个个给每一个部件进行烧写, 烧写方式可能还不尽相同。所以我引入了一个机制, 叫“BL 的分散烧录”。 首先我们将所有的固件拼装成一个大固件(依次数据拼接), 并将这个大固件预先批量烧录到外扩 ROM 中, 比如spiFlash; 再将主 MCU 预先烧录好 BL; 然后进行SMT 焊接。PCBA生产出来之后, 只要一上测试工装(首次上电),BL 会去外扩 ROM 中读取大固件, 并从中分离出各个小固件, 分别以相应的接口烧录到各个部件中去。配合工装的测试命令, 直接进行自检。这样做, 批量化生产是非常高效的。当然, 这个 BL 开发起来也会有一定难度, 最大问题可能还是各个部件烧录接口的实现( 有些部件的烧录协 议 是 比 较 复 杂 的, 比 如 STM32 的SWD 或者 ESP8266 的SLIP)。 OK, 上面对一些 BL 实例的实现和应用场景进行了介绍。还有一些实例没有介绍, 比如通过 CAN 总线或SPI 进行文件传输, 这个我们还是放到专门的章节去详细讲解。当然, 各位读者可以在此基础上衍生出更多有特色而又实用的 BL 来。 BL 没有最好的, 只有最适合自己的。通常来说, 我们并不会把 BL 设计得非常复杂, 原则上它应该尽量短小精炼, 以便为 APP 区节省出更多的 ROM 空间。毕竟不能喧宾夺主, APP才是产品的主角。 4. 不走寻常路的BL 4.1 Bootpatcher 我来问大家一个问题:“Bootloader 在 ROM 中的位置一定是在 APP 区前面吗?”很显然不是,AVR 就是最好的例子。那如果我们限定是STM32 呢? 似乎是的。上电复位一定是从0X08000000 位置开始运行的, 而且 BL 一定是先于 APP 运行的。 在某些特殊的情况下, 如果 APP 必须要放在0X08000000 位置上的话, 请问还有办法实现 BL 串口烧录吗? 要知道 APP 在运行的时候, 是不能IAP 自己的程序存储器的(就是自己不能擦除自己来烧录新固件), 如图7.32 所示。 APP 运行时, 想要重新烧录自身, 它可以直接跳转到后面的BL 上,BL 运行起来之后开始接收固件文件, 暂存校验 OK 之后, 将固件写入到前面的 APP 区。然后跳转到0X08000000, 或者直接重启。这样新的 APP 就运行起来了。这个位于 APP 后面的 BL, 我们称之为 Bootpatcher(意为启动补丁)。但是这种做法是有风险的, 一旦 APP 区烧录失败, 那产品就变砖了。所以这种方法一般不用。 4.2 APP 反烧BL 前面我们都是在讲 BL 烧录 APP, 那如果 BL 需要升级怎么办呢? 用JLINK。不错, 不过有更直接的方法, 如图7.33 所示。 这是一种逆向思维, 我们在 APP 程序中也实现接收固件文件, 暂存校验, 然后将其烧录到BL 区。这种做法与 Bootpatcher 同理, 也是有一定风险的, 但一般都没有问题。

    03-12 162浏览
  • 单片机开发:一文吃透交叉编译

    解锁单片机开发新姿势:一文吃透交叉编译

    02-17 391浏览
  • 搞定pic单片机IO口操作

    对于pic单片机的学习,很多朋友总是能充满激情,不断利用闲余时间研究pic单片机的各类技术。而谈及pic单片机,必须牵扯至51、AVR单片机。因此本文中,将探讨pic单片机以及51、AVR单片机对于IO口的操作。对于本文,希...

    2024-12-12 213浏览
  • 单片机时序图基础知识

    单片机是通过什么样的办法进行取指令,执行指令和其它操作的呢?在这里引入了一个时序的概念。

    2024-09-13 690浏览
  • 单片机串口自动识别波特率

    现在的单片机资源越来越丰富了,其中我们常用的串口也是内部集成了多个,关键功能也越来越强了。 我们有些应用可能会用到串口自动识别波特率,今天就来讲讲MCU串口自动识别波特率底层的常见的原理,以及MCU的案例。 自动识别波特率常见原理 串口自动识别波特率(Auto Baud Rate Detection,简称ABR)的底层原理主要基于串口通信中数据的传输特性和设备间的交互机制。以下是几种常见的自动波特率识别原理。 起始位和停止位检测法: 原理:串口通信中,每个数据包的开始都会有一个起始位(通常为0),结束有一个或多个停止位(通常为1)。通过检测这两个位的时间间隔,可以计算出波特率。 特定模式检测法: 原理:发送设备发送一系列具有特定模式的字节(如固定的字节序列或模式),接收设备尝试以不同的波特率接收并检测这些模式。当检测到与发送模式匹配的数据时,即可确定当前的波特率。 波特率扫描法: 原理:发送设备在初始化阶段发送一个包含多个不同波特率的扫描命令。接收设备在接收到这些命令后,会自动检测并匹配最接近的波特率。 周期性信号检测法: 原理:利用信号的周期性变化进行波特率检测。通过分析信号的频率和周期性特征,可以推算出波特率。 MCU硬件串口自动识别波特率 现在市面上很多新推出的MCU都有波特率自动识别的功能,这里以瑞萨RA系列单片机为例,配合e2 studio给大家讲讲串口自动识别波特率的配置。 1 UART1配置 UART1的配置只需要按照应用要求来做就可以,但必须留意所选的RXD脚必须跟IRQ是复用的,以便做软件切换。由于是做波特率自适应,属性页面中关于Baud的配置可以忽略。 2 GPT配置 由于需要通过定时器来做start bit的时间间隔测量,可以选用GPT/AGT使用one-shot/Periodic模式并留足够Period值余量来确保start bit在低速下不会溢出。 3 P402管脚配置为IRQ4 将SCI1串口默认的(RXD)P402管脚改为IRQ4,并添加中断入口函数。 中断入口函数里面首先判断是否发生了下降沿触发,然后启动定时器,等再次中断进入后,停止定时器,并取得定时器计数值,通过跟默认已知的系统时钟参数相除,就能得到确切的波特率数值。 4 波特率计算和相关寄存器配置 取得波特率实际值后,通过硬件手册上的相关的方程式就能够反推出几个控制波特率的寄存器的配置需要值 (brr,  semr , cks等),并将相关计算出的数值修改到FSP SCI1的全局变量结构体内 (g_uart1_baud_setting)。 5 修改P402管脚配置为UART-RXD 然后将P402管脚修改为UART功能,并启动 UART_Open() 函数,设置波特率已配置标志。 6 这样主函数就能够直接通过已侦测到的波特率直接发送数据和开始接收数据。 上述方法只需通过底层寄存器配置就能使简单快速做波特率侦测、计算和配置。通过这种方式,只要芯片系统时钟符合范围要求,任何非通用或者极高速/超低速的波特率也是能使实现侦测并配置使用。 

    2024-08-02 234浏览
  • netlink通信数据结构

    一:netlink通信数据结构 (一)netlink消息报头:struct nlmsghdr struct nlmsghdr { __u32 nlmsg_len; /* Length of message including header */ __u16 nlmsg_type; /* Message content */ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq; /* Sequence number */ __u32 nlmsg_pid; /* Sending process port ID */ }; netlink消息同TCP/UDP消息一样,也需要遵循协议要求的格式,每个netlink消息的开头是固定长度的netlink报头,报头后才是实际的载荷。 netlink报头一共占16个字节,具体内容即同struct nlmsghdr中定义的一样。 (1)nlmsg_len:整个netlink消息的长度(包含消息头); (2)nlmsg_type:消息状态,内核在include/uapi/linux/netlink.h中定义了以下4种通用的消息类型,它们分别是: NLMSG_NOOP:不执行任何动作,必须将该消息丢弃; NLMSG_ERROR:消息发生错误; NLMSG_DONE:标识分组消息的末尾; NLMSG_OVERRUN:缓冲区溢出,表示某些消息已经丢失。 除了这4种类型的消息以外,不同的netlink协议也可以自行添加自己所特有的消息类型,但是内核定义了类型保留宏(#define NLMSG_MIN_TYPE 0x10),即小于该值的消息类型值由内核保留,不可用。 (3)nlmsg_flags:消息标记,它们用以表示消息的类型,同样定义在include/uapi/linux/netlink.h中; #define NLM_F_REQUEST 1 /* It is request message. */ #define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */ #define NLM_F_ACK 4 /* Reply with ack, with zero or error code */ #define NLM_F_ECHO 8 /* Echo this request */ #define NLM_F_DUMP_INTR 16 /* Dump was inconsistent due to sequence change */ /* Modifiers to GET request */ #define NLM_F_ROOT 0x100 /* specify tree root */ #define NLM_F_MATCH 0x200 /* return all matching */ #define NLM_F_ATOMIC 0x400 /* atomic GET */ #define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH) /* Modifiers to NEW request */ #define NLM_F_REPLACE 0x100 /* Override existing */ #define NLM_F_EXCL 0x200 /* Do not touch, if it exists */ #define NLM_F_CREATE 0x400 /* Create, if it does not exist */ #define NLM_F_APPEND 0x800 /* Add to end of list */ (4)nlmsg_seq:消息序列号,用以将消息排队,有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用; (5)nlmsg_pid:发送端口的ID号,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号。 (二)socket消息数据包结构:struct msghdr struct user_msghdr { void __user *msg_name; /* ptr to socket address structure */ int msg_namelen; /* size of socket address structure */ struct iovec __user *msg_iov; /* scatter/gather array */ __kernel_size_t msg_iovlen; /* # elements in msg_iov */ void __user *msg_control; /* ancillary data */ __kernel_size_t msg_controllen; /* ancillary data buffer length */ unsigned int msg_flags; /* flags on received message */ }; 应用层向内核传递消息可以使用sendto()或sendmsg()函数,其中sendmsg函数需要应用程序手动封装msghdr消息结构,而sendto()函数则会由内核代为分配。其中 (1)msg_name:指向数据包的目的地址; (2)msg_namelen:目的地址数据结构的长度; (3)msg_iov:消息包的实际数据块,定义如下: struct iovec { void *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ }; iov_base:消息包实际载荷的首地址; iov_len:消息实际载荷的长度。 (4)msg_control:消息的辅助数据; (5)msg_controllen:消息辅助数据的大小; (6)msg_flags:接收消息的标识。 对于该结构,我们更需要关注的是前三个变量参数,对于netlink数据包来说其中msg_name指向的就是目的sockaddr_nl地址结构实例的首地址,iov_base指向的就是消息实体中的nlmsghdr消息头的地址,而iov_len赋值为nlmsghdr中的nlmsg_len即可(消息头+实际数据)。 (三)netlink消息处理宏 #define NLMSG_ALIGNTO 4U #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) ) /* 对len执行4字节对齐 */ #define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr))) /* netlink消息头长度 */ #define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN) /* netlink消息载荷len加上消息头 */ #define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len)) /* 对netlink消息全长执行字节对齐 */ #define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0))) /* 获取netlink消息实际载荷位置 */ #define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \ (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))/* 取得下一个消息的首地址,同时len也减少为剩余消息的总长度 */ #define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \ (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \ (nlh)->nlmsg_len <= (len)) /* 验证消息的长度 */ #define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len))) /* 返回PAYLOAD的长度 */ Linux为了处理netlink消息方便,在 include/uapi/linux/netlink.h中定义了以上消息处理宏,用于各种场合。对于Netlink消息来说,处理如下格式(见netlink.h): /* ======================================================================== * Netlink Messages and Attributes Interface (As Seen On TV) * ------------------------------------------------------------------------ * Messages Interface * ------------------------------------------------------------------------ * * Message Format: * <--- nlmsg_total_size(payload) ---> * <-- nlmsg_msg_size(payload) -> * +----------+- - -+-------------+- - -+-------- - - * | nlmsghdr | Pad | Payload | Pad | nlmsghdr * +----------+- - -+-------------+- - -+-------- - - * nlmsg_data(nlh)---^ ^ * nlmsg_next(nlh)-----------------------+ * * Payload Format: * <---------------------- nlmsg_len(nlh) ---------------------> *<- nlmsg_attrlen(nlh, hdrlen) -> * +----------------------+- - -+--------------------------------+ * | Family Header | Pad | Attributes | * +----------------------+- - -+--------------------------------+ * nlmsg_attrdata(nlh, hdrlen)---^ * * ------------------------------------------------------------------------ * Attributes Interface * ------------------------------------------------------------------------ * * Attribute Format: * <------- nla_total_size(payload) -------> * <---- nla_attr_size(payload) -----> * +----------+- - -+- - - - - - - - - +- - -+-------- - - * | Header | Pad | Payload | Pad | Header * +----------+- - -+- - - - - - - - - +- - -+-------- - - * <- nla_len(nla) -> ^ * nla_data(nla)----^ | * nla_next(nla)-----------------------------' * *========================================================================= */ 二:应用层向内核发送netlink消息 (一)应用层向内核发送netlink消息示例 使用如下示例程序可向内核netlink套接字发送消息: #define TEST_DATA_LEN 16 #DEFINE TEST_DATA "netlink send test" /* 仅作为示例,内核NETLINK_ROUTE套接字无法解析 */ struct sockaddr_nl nladdr; struct msghdr msg; struct nlmsghdr *nlhdr; struct iovec iov; /* 填充目的地址结构 */ memset(&nladdr, 0, sizeof(nladdr)); nladdr.nl_family = AF_NETLINK; nladdr.nl_pid = 0; /* 地址为内核 */ nladdr.nl_groups = 0; /* 单播 */ /* 填充netlink消息头 */ nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(TEST_DATA_LEN)); nlhdr->nlmsg_len = NLMSG_LENGTH(TEST_DATA_LEN); nlhdr->nlmsg_flags = NLM_F_REQUEST; nlhdr->nlmsg_pid = get_pid(); /* 当前套接字所绑定的ID号(此处为本进程的PID) */ nlhdr->nlmsg_seq = 0; /* 填充netlink消息实际载荷 */ strcpy(NLMSG_DATA(nlhdr), TEST_DATA); iov.iov_base = (void *)nlhdr; iov.iov_len = nlhdr->nlmsg_len; /* 填充数据消息结构 */ memset(&msg, 0, sizeof(msg)); msg.msg_name = (void *)&(nladdr); msg.msg_namelen = sizeof(nladdr); msg.msg_iov = &iov; msg.msg_iovlen = 1; /* 发送netlink消息 */ sendmsg (sock, &msg, 0); /* sock描述符见《Netlink 内核实现分析(一):创建》,为NETLINK_ROUTE类型套接字 */ 这里列出了一个调用sendmsg向内核发送消息的示例代码片段(仅作为示例,发送的消息内核netlink套接字可能无法解析)。首先初始化目的地址数据结构,设置nl_pid和nl_groups为0指定消息的目的地址为内核;然后初始化netlink消息头指明消息的长度为TEST_DATA_LEN + NLMSG_ALIGN(sizeof(struct nlmsghdr))(包含消息头),发送端的ID号为发送socket消息所绑定的ID号(这样内核才知道消息是谁发送的);然后设置消息的实际载荷,将数据拷贝到紧接消息头后的实际载荷部分;最后组装成msg消息就可以调用sendmsg向内核发送了。 (二)sendmsg系统调用流程(发送) 下面跟随内核的sendmsg系统调用的整个流程来分析消息是如何被送到内核的(需要注意的是,在不使用NETLINK_MMAP技术的情况下,整个发送的过程中存在1~2次数据的内存拷贝动作,后面会逐一点出!): 应用层向内核发送消息是通过函数sendmsg, 对应的系统调用如下: SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags) { if (flags & MSG_CMSG_COMPAT) return -EINVAL; return __sys_sendmsg(fd, msg, flags); } long __sys_sendmsg(int fd, struct user_msghdr __user *msg, unsigned flags) { int fput_needed, err; struct msghdr msg_sys; struct socket *sock; sock = sockfd_lookup_light(fd, &err, &fput_needed);    //通过fd描述符找到对应的socket套接字结构实例 if (!sock) goto out; err = ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL, 0);//----- 详解1 fput_light(sock->file, fput_needed); out: return err; } 1. ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL, 0); 传入的参数中第三个和最后一个需要关注一下,其中第三个它是一个内核版的socket消息数据包结构,同应用层的略有不同,定义如下: struct msghdr { void *msg_name; /* 同应用层 */ int msg_namelen; /* 同应用层 */ struct iov_iter msg_iter; /*为msg_iov和msg_iovlen的合体 */ void *msg_control; /* 同应用层 */ __kernel_size_t msg_controllen; /* 同应用层 */ unsigned int msg_flags; /* 同应用层 */ struct kiocb *msg_iocb; /*用于异步请求*/ }; 其中___sys_sendmsg函数原型如下: static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg, struct msghdr *msg_sys, unsigned int flags, struct used_address *used_address, unsigned int allowed_msghdr_flags) { struct compat_msghdr __user *msg_compat = (struct compat_msghdr __user *)msg; struct sockaddr_storage address; struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;------ 详解2 unsigned char ctl[sizeof(struct cmsghdr) + 20] __attribute__ ((aligned(sizeof(__kernel_size_t)))); /* 20 is size of ipv6_pktinfo */ unsigned char *ctl_buf = ctl; int ctl_len; ssize_t err; msg_sys->msg_name = &address; if (MSG_CMSG_COMPAT & flags)----- 详解3 err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov); else err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov); if (err < 0) return err; err = -ENOBUFS; if (msg_sys->msg_controllen > INT_MAX) goto out_freeiov; flags |= (msg_sys->msg_flags & allowed_msghdr_flags); ctl_len = msg_sys->msg_controllen; if ((MSG_CMSG_COMPAT & flags) && ctl_len) { err = cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl, sizeof(ctl)); if (err) goto out_freeiov; ctl_buf = msg_sys->msg_control; ctl_len = msg_sys->msg_controllen; } else if (ctl_len) { if (ctl_len > sizeof(ctl)) { ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL); if (ctl_buf == NULL) goto out_freeiov; } err = -EFAULT; /* * Careful! Before this, msg_sys->msg_control contains a user pointer. * Afterwards, it will be a kernel pointer. Thus the compiler-assisted * checking falls down on this. */ if (copy_from_user(ctl_buf, (void __user __force *)msg_sys->msg_control, ctl_len)) goto out_freectl; msg_sys->msg_control = ctl_buf; } msg_sys->msg_flags = flags;//保存用户传递的flag标识 if (sock->file->f_flags & O_NONBLOCK)//如果当前的socket已经被配置为非阻塞模式则置位MSG_DONTWAIT标识 msg_sys->msg_flags |= MSG_DONTWAIT; /* * If this is sendmmsg() and current destination address is same as * previously succeeded address, omit asking LSM's decision. * used_address->name_len is initialized to UINT_MAX so that the first * destination address never matches. */ ------------------ 详解4 if (used_address && msg_sys->msg_name && used_address->name_len == msg_sys->msg_namelen && !memcmp(&used_address->name, msg_sys->msg_name, used_address->name_len)) { err = sock_sendmsg_nosec(sock, msg_sys); goto out_freectl; } err = sock_sendmsg(sock, msg_sys); /* * If this is sendmmsg() and sending to current destination address was * successful, remember it. */ if (used_address && err >= 0) {----------- 详解5 used_address->name_len = msg_sys->msg_namelen; if (msg_sys->msg_name) memcpy(&used_address->name, msg_sys->msg_name, used_address->name_len); } out_freectl: if (ctl_buf != ctl) sock_kfree_s(sock->sk, ctl_buf, ctl_len); out_freeiov: kfree(iov); return err; } 函数__sendmsg参数是一个struct used_address结构体指针,这个结构体定义如下: struct used_address { struct sockaddr_storage name; unsigned int name_len; }; 这里的name字段用来存储消息的地址,name_len字段是消息地址的长度,它们同struct msghdr结构体的前两个字段一致。该结构体主要用与sendmmsg系统调用(用于同时向一个socket地址发送多个数据包,可以避免重复的网络security检查,从而提高发送效率)保存多个数据包的目的地址。现在这里设置为NULL,表示不使用。 2. struct iovec iovstack[UIO_FASTIOV], *iov = iovstack; 这里的iovstack数组是用来加速用户数据拷贝的(这里假定用户数据的iovec个数通常不会超过UIO_FASTIOV个,如果超过会通过kmalloc分配内存)。 3. (MSG_CMSG_COMPAT & flags) 判断flag中是否设置了32bit修正标识,从前文中系统调用的入口处已经可以看出了,这里显然不会设置该标识位,所以这里调用copy_msghdr_from_user函数将用户空间传入的消息(struct user_msghdr __user *msg)安全的拷贝到内核空间中(struct msghdr *msg_sys),函数定义如下: static int copy_msghdr_from_user(struct msghdr *kmsg, struct user_msghdr __user *umsg, struct sockaddr __user **save_addr, struct iovec **iov) { struct sockaddr __user *uaddr; struct iovec __user *uiov; size_t nr_segs; ssize_t err; /*调用access_ok检查用户数据的有效性,调用__get_user函数执行单数据的复制操作(并没有复制数据包内容)*/ if (!access_ok(VERIFY_READ, umsg, sizeof(*umsg)) || __get_user(uaddr, &umsg->msg_name) || __get_user(kmsg->msg_namelen, &umsg->msg_namelen) || __get_user(uiov, &umsg->msg_iov) || __get_user(nr_segs, &umsg->msg_iovlen) || __get_user(kmsg->msg_control, &umsg->msg_control) || __get_user(kmsg->msg_controllen, &umsg->msg_controllen) || __get_user(kmsg->msg_flags, &umsg->msg_flags)) return -EFAULT; if (!uaddr) kmsg->msg_namelen = 0; if (kmsg->msg_namelen < 0) return -EINVAL; if (kmsg->msg_namelen > sizeof(struct sockaddr_storage)) kmsg->msg_namelen = sizeof(struct sockaddr_storage); if (save_addr)// *save_addr = uaddr; if (uaddr && kmsg->msg_namelen) { if (!save_addr) { -------------- 详解6 err = move_addr_to_kernel(uaddr, kmsg->msg_namelen, kmsg->msg_name); if (err < 0) return err; } } else { kmsg->msg_name = NULL; kmsg->msg_namelen = 0; } if (nr_segs > UIO_MAXIOV) return -EMSGSIZE; kmsg->msg_iocb = NULL; return import_iovec(save_addr ? READ : WRITE, uiov, nr_segs,------- 详解7 UIO_FASTIOV, iov, &kmsg->msg_iter); } 4. if (!save_addr) { 如果用户消息中存在目的地址且入参save_addr为空(当前情景中正好就是这类情况),就调用move_addr_to_kernel()函数将消息地址拷贝到内核kmsg的结构中,否则将kmsg中的目的地址和长度字段置位空。接下来判断消息实际载荷iovec结构的个数,这里UIO_MAXIOV值定义为1024,也就是说消息数据iovec结构的最大个数不能超过这个值。 5. import_iovec 调用import_iovec()函数开始执行实际数据从用户态向内核态的拷贝动作(注意这里并没有拷贝用户空间实际消息载荷数据,仅仅检查了用户地址有效性并拷贝了长度等字段),在拷贝完成后,&kmsg->msg_iter中的数据初始化情况如下: int type:WRITE; size_t iov_offset:初始化为0; size_t count:所有iovec结构数据的总长度(即iov->iov_len的总和); const struct iovec *iov:首个iov结构指针; unsigned long nr_segs:iovec结构的个数。 6. used_address && msg_sys->msg_name && used_address->name_len == msg_sys->msg_namelen && !memcmp(&used_address->name, msg_sys->msg_name, used_address->name_len) 根据传入的used_address指针判断当前发送消息的目的地址是否同它记录的一致,如果一致则调用sock_sendmsg_nosec()函数发送数据,否则调用sock_sendmsg()函数发送数据,sock_sendmsg()其实最终也是通过调用sock_sendmsg_nosec()来发送数据的,它们的区别就在于是否调用安全检查函数,如下: int sock_sendmsg(struct socket *sock, struct msghdr *msg) { int err = security_socket_sendmsg(sock, msg, msg_data_left(msg)); return err ?: sock_sendmsg_nosec(sock, msg); ------ 详解8 } 7.used_address && err >= 0 在sendmmsg系统调用每一次发送多个消息时,由于发送的目的地一般都是一致的,所以只需要在发送第一个消息爆时执行检查就可以了,通过这种策略就可以加速数据的发送。最后,在发送完数据后,如果传入的used_address指针非空,就会将本次成功发送数据的目的地址记录下来,供下次发送数据比较。 8.详 sock_sendmsg_nosec static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg) { int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg)); BUG_ON(ret == -EIOCBQUEUED); return ret; } 这里调用了socket所绑定协议特有的数据发送钩子函数,其中最后一个参数为msg->msg_iter->count,即消息实际载荷的总长度。在前一篇文章中已经看到了对于netlink类型的套接字来说该函数被注册为netlink_sendmsg()。 static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len) { struct sock *sk = sock->sk; struct netlink_sock *nlk = nlk_sk(sk); DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);   //定义了一个struct sockaddr_nl *addr指针,它指向了msg->msg_name表示消息的目的地址(会做地址长度检查) u32 dst_portid; u32 dst_group; struct sk_buff *skb; int err; struct scm_cookie scm; u32 netlink_skb_flags = 0; if (msg->msg_flags&MSG_OOB) return -EOPNOTSUPP; err = scm_send(sock, msg, &scm, true);  //发送消息辅助数据 if (err < 0) return err; if (msg->msg_namelen) {--------------- 详解9 err = -EINVAL; if (addr->nl_family != AF_NETLINK) goto out; dst_portid = addr->nl_pid; dst_group = ffs(addr->nl_groups); err = -EPERM; if ((dst_group || dst_portid) && !netlink_allowed(sock, NL_CFG_F_NONROOT_SEND)) goto out; netlink_skb_flags |= NETLINK_SKB_DST; } else { dst_portid = nlk->dst_portid; dst_group = nlk->dst_group; } if (!nlk->bound) { --------------------- 详解10 err = netlink_autobind(sock); if (err) goto out; } else { /* Ensure nlk is hashed and visible. */ smp_rmb(); } err = -EMSGSIZE; if (len > sk->sk_sndbuf - 32)------------------- 详解11 goto out; err = -ENOBUFS; skb = netlink_alloc_large_skb(len, dst_group); if (skb == NULL) goto out; NETLINK_CB(skb).portid = nlk->portid; ------------------ 详解12 NETLINK_CB(skb).dst_group = dst_group; NETLINK_CB(skb).creds = scm.creds; NETLINK_CB(skb).flags = netlink_skb_flags; err = -EFAULT; if (memcpy_from_msg(skb_put(skb, len), msg, len)) { kfree_skb(skb); goto out; } err = security_netlink_send(sk, skb);----------------- 详解13 if (err) { kfree_skb(skb); goto out; } if (dst_group) { atomic_inc(&skb->users); netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);//组播方式 --- 详解15 } err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags&MSG_DONTWAIT);//单播方式 --- 详解14 out: scm_destroy(&scm); return err; } 9. msg->msg_namelen 这里如果用户指定了netlink消息的目的地址,则对其进行校验,然后判断当前netlink协议的NL_CFG_F_NONROOT_SEND标识是否设置,如果设置了该标识则允许非root用户发送组播,对于NETLINK_ROUTE类型的netlink套接字,并没有设置该标识,表明非root用户不能发送组播消息;然后设置NETLINK_SKB_DST标识。如果用户没有指定netlink消息的目的地址,则使用netlink套接字默认的(该值默认为0,会在调用connect系统调用时在netlink_connect()中被赋值为用户设置的值)。注意这里dst_group经过ffs的处理后转化为组播地址位数(找到最低有效位)。 10. !nlk->bound 接下来判断当前的netlink套接字是否被绑定过,如果没有绑定过这里调用netlink_autobind()进行动态绑定,该函数在前一篇文章中已经分析.继续往下分析 /* It's a really convoluted way for userland to ask for mmaped * sendmsg(), but that's what we've got... */ if (netlink_tx_is_mmaped(sk) && msg->msg_iter.type == ITER_IOVEC && msg->msg_iter.nr_segs == 1 && msg->msg_iter.iov->iov_base == NULL) { err = netlink_mmap_sendmsg(sk, msg, dst_portid, dst_group, &scm); goto out; } 如果内核配置了CONFIG_NETLINK_MMAP内核选项,则表示内核空间和应用层的消息发送队列支持内存映射,然后通过调用netlink_mmap_sendmsg来发送netlink消息,该种方式将减少数据的内存数据的拷贝动作,减少发送时间和资源占用。现我的环境中并不支持,继续往下分析: 11. if (len > sk->sk_sndbuf - 32) 接下来判断需要发送的数据是否过长(长于发送缓存大小),然后通过netlink_alloc_large_skb分配skb结构(传入的参数为消息载荷的长度以及组播地址)。 12. NETLINK_CB(skb).portid = nlk->portid; 在成功创建skb结构之后,这里就开始初始化它,这里使用到了skb中的扩展cb字段(char cb[48] __aligned(8),一共48个字节用于存放netlink的地址和标识相关的附加信息足够了),同时使用宏NETLINK_CB来操作这些字段。netlink将skb的cb字段强制定义为struct netlink_skb_parms结构: struct netlink_skb_parms { struct scm_creds creds; /* Skb credentials */ __u32 portid; __u32 dst_group; __u32 flags; struct sock *sk; }; 其中portid表示原端套接字所绑定的id,dst_group表示消息目的组播地址,flag为标识,sk指向原端套接字的sock结构。 这里首先将套接字绑定的portid赋值到skb的cb字段中、同时设置组播地址的数量以及netlink_skb标识(这里是已经置位NETLINK_SKB_DST)。接下来调用最关键的调用memcpy_from_msg拷贝数据,它首先调用skb_put调整skb->tail指针,然后执行copy_from_iter(data, len, &msg->msg_iter)将数据从msg->msg_iter中传输到skb->data中(这是第一次内存拷贝动作!将用户空间数据直接拷贝到内核skb中)。 13. security_netlink_send 调用security_netlink_send()执行security检查. 最后如果是组播发送则调用netlink_broadcast()发送消息,否则调用netlink_unicast()发送单播消息。 14. netlink_unicast 发送单播消息 int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 portid, int nonblock) { struct sock *sk; int err; long timeo; skb = netlink_trim(skb, gfp_any());---------- 详解16 timeo = sock_sndtimeo(ssk, nonblock); ---------------- 详解17 retry: sk = netlink_getsockbyportid(ssk, portid);--------------- 详解18 if (IS_ERR(sk)) { kfree_skb(skb); return PTR_ERR(sk); } if (netlink_is_kernel(sk))-------------- 详解19 return netlink_unicast_kernel(sk, skb, ssk); ------------ 详解20 if (sk_filter(sk, skb)) { err = skb->len; kfree_skb(skb); sock_put(sk); return err; } err = netlink_attachskb(sk, skb, &timeo, ssk); if (err == 1) goto retry; if (err) return err; return netlink_sendskb(sk, skb); } 15. netlink_trim 调用netlink_trim()重新裁剪skb的数据区的大小,这可能会clone出一个新的skb结构同时重新分配skb->data的内存空间(这就出现了第三次的内存拷贝动作!),当然如果原本skb中多余的内存数据区非常小或者该内存空间是在vmalloc空间中的就不会执行上述操作,我们现在跟随的情景上下文中就是后一种情况,并不会重新分配空间。 16. sock_sndtimeo 记下发送超时等待时间,如果已经设置了MSG_DONTWAIT标识,则等待时间为0,否则返回sk->sk_sndtimeo(该值在sock初始化时由sock_init_data()函数赋值为MAX_SCHEDULE_TIMEOUT)。 17. netlink_getsockbyportid 接下来调用netlink_getsockbyportid根据目的portid号和原端sock结构查找目的端的sock结构。其定义如下: static struct sock *netlink_getsockbyportid(struct sock *ssk, u32 portid) { struct sock *sock; struct netlink_sock *nlk; /*调用netlink_lookup执行查找工作,查找的命名空间和协议号同原端sock,它会从nl_table[protocol]的哈希表中找到已经注册的目的端sock套接字。找到以后执行校验,如若找到的socket已经connect了,则它的目的portid必须是原端的portid*/ sock = netlink_lookup(sock_net(ssk), ssk->sk_protocol, portid); if (!sock) return ERR_PTR(-ECONNREFUSED); /* Don't bother queuing skb if kernel socket has no input function */ nlk = nlk_sk(sock); if (sock->sk_state == NETLINK_CONNECTED && nlk->dst_portid != nlk_sk(ssk)->portid) { sock_put(sock); return ERR_PTR(-ECONNREFUSED); } return sock; } 18. netlink_is_kernel(sk) 判断目的的netlink socket是否是内核的netlink socket,如果目的地址是内核空间,则调用netlink_unicast_kernel向内核进行单播,入参是目的sock、原端sock和数据skb。目前目的地址是内核。 函数netlink_is_kernel定义如下: static inline int netlink_is_kernel(struct sock *sk) { return nlk_sk(sk)->flags & NETLINK_F_KERNEL_SOCKET; } 19. netlink_unicast_kernel 函数netlink_unicast_kernel定义如下: static int netlink_unicast_kernel(struct sock *sk, struct sk_buff *skb, struct sock *ssk) { int ret; struct netlink_sock *nlk = nlk_sk(sk); ret = -ECONNREFUSED; if (nlk->netlink_rcv != NULL) { ret = skb->len; netlink_skb_set_owner_r(skb, sk); NETLINK_CB(skb).sk = ssk; netlink_deliver_tap_kernel(sk, ssk, skb); nlk->netlink_rcv(skb); consume_skb(skb); } else { kfree_skb(skb); } sock_put(sk); return ret; } 检查目标netlink套接字是否注册了netlink_rcv()接收函数,如果没有则直接丢弃该数据包,否则继续发送流程,这里首先设置一些标识: skb->sk = sk; /* 将目的sock赋值给skb->sk指针 */ skb->destructor = netlink_skb_destructor; /* 注册destructor钩子函数 */ NETLINK_CB(skb).sk = ssk; /* 将原端的sock保存早skb的cb扩展字段中 */ 最后就调用了nlk->netlink_rcv(skb)函数将消息送到内核中的目的netlink套接字中了。在前一篇文章中已经看到在内核注册netlink套接字的时候已经将其接收函数注册到了netlink_rcv中: struct sock * __netlink_kernel_create(struct net *net, int unit, struct module *module, struct netlink_kernel_cfg *cfg) { ...... if (cfg && cfg->input) nlk_sk(sk)->netlink_rcv = cfg->input; ... } 对于NETLINK_ROUTE类型的套接字来说就是rtnetlink_rcv了,netlink_rcv()钩子函数会接收并解析用户传下来的数据,不同类型的netlink协议各不相同,这里就不进行分析了。至此应用层下发单播的netlink数据就下发完成了。 20. netlink_broadcast() 发送组播消息 int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 portid, u32 group, gfp_t allocation) { return netlink_broadcast_filtered(ssk, skb, portid, group, allocation, NULL, NULL); } 函数netlink_broadcast_filtered定义如下: int netlink_broadcast_filtered(struct sock *ssk, struct sk_buff *skb, u32 portid, u32 group, gfp_t allocation, int (*filter)(struct sock *dsk, struct sk_buff *skb, void *data), void *filter_data) { struct net *net = sock_net(ssk); struct netlink_broadcast_data info; struct sock *sk; skb = netlink_trim(skb, allocation); //初始化netlink组播数据结构netlink_broadcast_data info.exclude_sk = ssk; info.net = net; info.portid = portid; info.group = group;//保存了目的组播地址 info.failure = 0; info.delivery_failure = 0; info.congested = 0; info.delivered = 0; info.allocation = allocation; info.skb = skb; info.skb2 = NULL; info.tx_filter = filter; info.tx_data = filter_data; /* While we sleep in clone, do not allow to change socket list */ netlink_lock_table(); sk_for_each_bound(sk, &nl_table[ssk->sk_protocol].mc_list)//从nl_table[ssk->sk_protocol].mc_list里边查找加入组播组的socket do_one_broadcast(sk, &info);//依次发送组播数据 consume_skb(skb); netlink_unlock_table(); if (info.delivery_failure) { kfree_skb(info.skb2); return -ENOBUFS; } consume_skb(info.skb2); if (info.delivered) { if (info.congested && gfpflags_allow_blocking(allocation)) yield(); return 0; } return -ESRCH; } 这里首先初始化netlink组播数据结构netlink_broadcast_data,其中info.group中保存了目的组播地址,然后从nl_table[ssk->sk_protocol].mc_list里边查找加入组播组的socket,并调用do_one_broadcast()函数依次发送组播数据: static void do_one_broadcast(struct sock *sk, struct netlink_broadcast_data *p) { struct netlink_sock *nlk = nlk_sk(sk); int val; //做必要的检查 if (p->exclude_sk == sk) return; ... val = netlink_broadcast_deliver(sk, p->skb2);//对目的sock发送数据skb ... } 当然,在发送之前会做一些必要的检查,例如这里会确保原端sock和目的端sock不是同一个,它们属于同一个网络命名空间,目的的组播地址为发送的目的组播地址等等,然后会对skb和组播数据结构netlink_broadcast_data进行一些处理,最后调用 netlink_broadcast_deliver()函数对目的sock发送数据skb: static int netlink_broadcast_deliver(struct sock *sk, struct sk_buff *skb) { struct netlink_sock *nlk = nlk_sk(sk); if (atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf && !test_bit(NETLINK_S_CONGESTED, &nlk->state)) { netlink_skb_set_owner_r(skb, sk); __netlink_sendskb(sk, skb); return atomic_read(&sk->sk_rmem_alloc) > (sk->sk_rcvbuf >> 1); } return -1; } static int __netlink_sendskb(struct sock *sk, struct sk_buff *skb) { int len = skb->len; netlink_deliver_tap(skb); skb_queue_tail(&sk->sk_receive_queue, skb);//将要发送的skb添加到目的sock的接收队列末尾 sk->sk_data_ready(sk);//通知钩子函数,告知目的sock有数据到达,执行处理流程 ----- 补充1 return len; } 补充1:可以看到,这里将要发送的skb添加到目的sock的接收队列末尾,然后调用sk_data_ready()通知钩子函数,告知目的sock有数据到达,执行处理流程。对于内核的netlink来说内核netlink的创建函数中已经将其注册为: struct sock * __netlink_kernel_create(struct net *net, int unit, struct module *module, struct netlink_kernel_cfg *cfg) { ...... sk->sk_data_ready = netlink_data_ready; ...... } static void netlink_data_ready(struct sock *sk) { BUG(); } 非常明显了,内核netlink套接字是无论如何也不应该接收到组播消息的。但是对于应用层netlink套接字,该sk_data_ready()钩子函数在初始化netlink函数sock_init_data()中被注册为sock_def_readable(),这个函数待分析。 三:内核接收应用层消息 当进程有数据发送过来时,内核部分会接收数据,上送的包是struct sk_buff *skb,我们可以通过netlink提供的一系列操作函数来获取消息头以及数据。 消息头 = nlmsg_hdr(skb); 消息数据 = NLMSG_DATA(nlh); 四:内核向应用层发送消息 (一)内核发送netlink单播消息 内核可以通过nlmsg_unicast()函数向应用层发送单播消息,由各个netlink协议负责调用,也有的协议是直接调用netlink_unicast()函数,其实nlmsg_unicast()也仅是netlink_unicast()的一个封装而已: /** * nlmsg_unicast - unicast a netlink message * @sk: netlink socket to spread message to * @skb: netlink message as socket buffer * @portid: netlink portid of the destination socket */ static inline int nlmsg_unicast(struct sock *sk, struct sk_buff *skb, u32 portid) { int err; err = netlink_unicast(sk, skb, portid, MSG_DONTWAIT); if (err > 0) err = 0; return err; } 这里以非阻塞(MSG_DONTWAIT)的形式向应用层发送消息,这时的portid为应用层套接字所绑定的id号。我们再次进入到netlink_unicast()内部,这次由于目的sock不再是内核,所以要走不同的的分支了 int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 portid, int nonblock) { struct sock *sk; int err; long timeo; skb = netlink_trim(skb, gfp_any()); timeo = sock_sndtimeo(ssk, nonblock); retry: sk = netlink_getsockbyportid(ssk, portid); if (IS_ERR(sk)) { kfree_skb(skb); return PTR_ERR(sk); } if (netlink_is_kernel(sk)) return netlink_unicast_kernel(sk, skb, ssk);  //应用层向内核发送消息 /*以下为内核向应用层发送消息的flow*/ if (sk_filter(sk, skb)) {//首先sk_filter执行防火墙的过滤,确保可以发送以后调用netlink_attachskb将要发送的skb绑定到netlink sock上 err = skb->len; kfree_skb(skb); sock_put(sk); return err; } err = netlink_attachskb(sk, skb, &timeo, ssk);---------------- 详解1 if (err == 1)//若执行netlink_attachskb()的返回值为1,就会再次尝试发送操作 goto retry; if (err) return err; return netlink_sendskb(sk, skb); ------------------- 详解2 } 1. netlink_attachskb 这里首先sk_filter执行防火墙的过滤,确保可以发送以后,调用netlink_attachskb将要发送的skb绑定到netlink sock上。 如果目的sock的接收缓冲区剩余的的缓存大小小于已经提交的数据量,或者标志位已经置位了阻塞标识NETLINK_CONGESTED,这表明数据不可以立即的送到目的端的接收缓存中。 因此,在原端不是内核socket且没有设置非阻塞标识的情况下会定义一个等待队列并等待指定的时间并返回1,否则直接丢弃该skb数据包并返回失败。 int netlink_attachskb(struct sock *sk, struct sk_buff *skb, long *timeo, struct sock *ssk) { struct netlink_sock *nlk; nlk = nlk_sk(sk); if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(NETLINK_S_CONGESTED, &nlk->state))) { DECLARE_WAITQUEUE(wait, current); if (!*timeo) { if (!ssk || netlink_is_kernel(ssk)) netlink_overrun(sk); sock_put(sk); kfree_skb(skb); return -EAGAIN; } __set_current_state(TASK_INTERRUPTIBLE); add_wait_queue(&nlk->wait, &wait); if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(NETLINK_S_CONGESTED, &nlk->state)) && !sock_flag(sk, SOCK_DEAD)) *timeo = schedule_timeout(*timeo); __set_current_state(TASK_RUNNING); remove_wait_queue(&nlk->wait, &wait); sock_put(sk); if (signal_pending(current)) { kfree_skb(skb); return sock_intr_errno(*timeo); } return 1; } netlink_skb_set_owner_r(skb, sk);//目的端的接收缓存区空间足够,就会调用netlink_skb_set_owner_r进行绑定 return 0; } 2. netlink_sendskb 调用netlink_sendskb()执行发送操作 int netlink_sendskb(struct sock *sk, struct sk_buff *skb) { int len = __netlink_sendskb(sk, skb); sock_put(sk); return len; } /*这里又一次回到了__netlink_sendskb函数执行发送流程*/ static int __netlink_sendskb(struct sock *sk, struct sk_buff *skb) { int len = skb->len; netlink_deliver_tap(skb); skb_queue_tail(&sk->sk_receive_queue, skb); sk->sk_data_ready(sk); return len; } 这里的sk_data_ready()钩子函数在初始化netlink函数sock_init_data()中被注册为sock_def_readable(): static void sock_def_readable(struct sock *sk) { struct socket_wq *wq; rcu_read_lock(); wq = rcu_dereference(sk->sk_wq); if (wq_has_sleeper(wq)) wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI | POLLRDNORM | POLLRDBAND); sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);//唤醒目的接收端socket的等待队列,这样应用层套接字就可以接收并处理消息了 rcu_read_unlock(); } (二)内核发送netlink组播消息 内核发送多播消息是通过函数nlmsg_multicast(),详细分析见上文,不再重复。 static inline int nlmsg_multicast(struct sock *sk, struct sk_buff *skb, u32 portid, unsigned int group, gfp_t flags) { int err; NETLINK_CB(skb).dst_group = group; err = netlink_broadcast(sk, skb, portid, group, flags); if (err > 0) err = 0; return err; } 五:应用层接收内核的消息 使用如下示例程序可以以阻塞的方式接收内核发送的netlink消息: #define TEST_DATA_LEN 16 struct sockaddr_nl nladdr; struct msghdr msg; struct nlmsghdr *nlhdr; struct iovec iov; /* 清空源地址结构 */ memset(&nladdr, 0, sizeof(nladdr)); /* 清空netlink消息头 */ nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(TEST_DATA_LEN)); memset(nlhdr, 0, NLMSG_SPACE(TEST_DATA_LEN)); /* 封装netlink消息 */ iov.iov_base = (void *)nlhdr; /* 接收缓存地址 */ iov.iov_len = NLMSG_LENGTH(TEST_DATA_LEN);; /* 接收缓存大小 */ /* 填充数据消息结构 */ memset(&msg, 0, sizeof(msg)); msg.msg_name = (void *)&(nladdr); msg.msg_namelen = sizeof(nladdr); /* 地址长度由内核赋值 */ msg.msg_iov = &iov; msg.msg_iovlen = 1; /* 接收netlink消息 */ recvmsg(sock_fd, &msg, 0); 本示例程序同前文中的发送程序类似,需要有接收端组装接收msg消息。同发送流程的不同之处在于: (1)msg.msg_name地址结构中存放的是消息源的地址信息,由内核负责填充。 (2)iov.iov_base为接收缓存的地址空间,其需要在接收前清空。 (3)iov.iov_len为单个iov接收缓存的长度,需要指明。 (4)msg.msg_namelen:为地址占用长度,有内核负责填充。 (5)msg.msg_iovlen:为接收iov空间的个数,需要指明。 这里用到了recvmsg系统调用,现进入该系统调用分析消息的整个接收的过程(需要注意的是,在不使用NETLINK_MMAP技术的情况下,整个接收的过程中存在1次数据的内存拷贝动作!): 应用层通过API recvmsg接收内核的消息,其对应的系统调用如下: SYSCALL_DEFINE3(recvmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags) { if (flags & MSG_CMSG_COMPAT) return -EINVAL; return __sys_recvmsg(fd, msg, flags); } long __sys_recvmsg(int fd, struct user_msghdr __user *msg, unsigned flags) { int fput_needed, err; struct msghdr msg_sys; struct socket *sock; sock = sockfd_lookup_light(fd, &err, &fput_needed);//也是通过fd描述符查找对应的套接字socket结构 if (!sock) goto out; err = ___sys_recvmsg(sock, msg, &msg_sys, flags, 0); fput_light(sock->file, fput_needed); out: return err; } 同sendmsg系统调用类似,这里也同样首先通过fd描述符查找对应的套接字socket结构,然后调用___sys_recvmsg()执行实际的工作 static int ___sys_recvmsg(struct socket *sock, struct user_msghdr __user *msg, struct msghdr *msg_sys, unsigned int flags, int nosec) { struct compat_msghdr __user *msg_compat = (struct compat_msghdr __user *)msg; struct iovec iovstack[UIO_FASTIOV]; struct iovec *iov = iovstack;//定义了一个大小为8的iovstack数组缓存,用来加速消息处理 unsigned long cmsg_ptr; int len; ssize_t err; /* kernel mode address */ struct sockaddr_storage addr; /* user mode address pointers */ struct sockaddr __user *uaddr; int __user *uaddr_len = COMPAT_NAMELEN(msg);//获取用户空间的地址长度字段的地址 msg_sys->msg_name = &addr; if (MSG_CMSG_COMPAT & flags) err = get_compat_msghdr(msg_sys, msg_compat, &uaddr, &iov); else err = copy_msghdr_from_user(msg_sys, msg, &uaddr, &iov);//拷贝用户态msg中的数据到内核态msg_sys中 ------------- 详解1 if (err < 0) return err; cmsg_ptr = (unsigned long)msg_sys->msg_control; msg_sys->msg_flags = flags & (MSG_CMSG_CLOEXEC|MSG_CMSG_COMPAT); /* We assume all kernel code knows the size of sockaddr_storage */ msg_sys->msg_namelen = 0;//将地址的长度字段清零 /*根据nosec的值是否为0而调用sock_recvmsg_nosec()或sock_recvmsg()函数接收数据,nosec在recvmsg系统调用传入的为0,在recvmmsg系统能够调用接收多个消息时传入已经接受的消息个数*/ if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys, flags);------------ 详解2 if (err < 0) goto out_freeiov; len = err; //len保存了接收到数据的长度,然后将消息地址信息从内核空间拷贝到用户空间 if (uaddr != NULL) { err = move_addr_to_user(&addr, msg_sys->msg_namelen, uaddr, uaddr_len); if (err < 0) goto out_freeiov; } err = __put_user((msg_sys->msg_flags & ~MSG_CMSG_COMPAT), COMPAT_FLAGS(msg)); if (err) goto out_freeiov; if (MSG_CMSG_COMPAT & flags) err = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr, &msg_compat->msg_controllen);//将flag复制到用户空间 else err = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr, &msg->msg_controllen);//将消息辅助数据等复制到用户空间 if (err) goto out_freeiov; err = len; out_freeiov: kfree(iov); return err; } 1. copy_msghdr_from_user 调用copy_msghdr_from_user拷贝用户态msg中的数据到内核态msg_sys中。当然这里主要是为了接收内核的消息,用户空间并没有什么实际的数据,这里最主要的作用就是确定用户需要接收多少数据量。注意第三个参数已经不再是NULL了,而是指向了uaddr指针的地址。函数copy_msghdr_from_user: static int copy_msghdr_from_user(struct msghdr *kmsg, struct user_msghdr __user *umsg, struct sockaddr __user **save_addr, struct iovec **iov) { struct sockaddr __user *uaddr; struct iovec __user *uiov; size_t nr_segs; ssize_t err; if (!access_ok(VERIFY_READ, umsg, sizeof(*umsg)) || __get_user(uaddr, &umsg->msg_name) || __get_user(kmsg->msg_namelen, &umsg->msg_namelen) || __get_user(uiov, &umsg->msg_iov) || __get_user(nr_segs, &umsg->msg_iovlen) || __get_user(kmsg->msg_control, &umsg->msg_control) || __get_user(kmsg->msg_controllen, &umsg->msg_controllen) || __get_user(kmsg->msg_flags, &umsg->msg_flags)) return -EFAULT; if (!uaddr) kmsg->msg_namelen = 0; if (kmsg->msg_namelen < 0) return -EINVAL; if (kmsg->msg_namelen > sizeof(struct sockaddr_storage)) kmsg->msg_namelen = sizeof(struct sockaddr_storage); if (save_addr) *save_addr = uaddr; if (uaddr && kmsg->msg_namelen) { if (!save_addr) { err = move_addr_to_kernel(uaddr, kmsg->msg_namelen, kmsg->msg_name); if (err < 0) return err; } } else { kmsg->msg_name = NULL; kmsg->msg_namelen = 0; } if (nr_segs > UIO_MAXIOV) return -EMSGSIZE; kmsg->msg_iocb = NULL; return import_iovec(save_addr ? READ : WRITE, uiov, nr_segs, UIO_FASTIOV, iov, &kmsg->msg_iter); } 传入的uaddr指针被指向了用户空间msg->msg_name地址处,然后内核也不再会调用move_addr_to_kernel将用户空间的消息地址字段拷贝到内核空间了(因为根本没必要了),然后以READ的方式调用import_iovec()函数,它会检查用户空间的消息数据地址是否可以写入,然后根据用户需要接收的msg_iovlen长度封装kmsg->msg_iter结构。 2. err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys, flags); recvmmsg()就是sock_recvmsg_nosec()的一个封装而已,只不过会增加security检查 int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags) { int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags); return err ?: sock_recvmsg_nosec(sock, msg, flags); } static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg, int flags) { return sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);//调用了接收套接字所在协议的recvmsg接收钩子函数,对于netlink就是netlink_recvmsg()函数 } netlink_recvmsg如下: static int netlink_recvmsg(struct socket *sock, struct msghdr *msg, size_t len, int flags) { struct scm_cookie scm; struct sock *sk = sock->sk; struct netlink_sock *nlk = nlk_sk(sk); int noblock = flags&MSG_DONTWAIT; size_t copied; struct sk_buff *skb, *data_skb; int err, ret; if (flags&MSG_OOB) return -EOPNOTSUPP; copied = 0; /*从接收socket的缓存中接收消息并通过skb返回,如果设置了MSG_DONTWAIT则在接收队列中没有消息时立即返回,否则会阻塞等待。*/ skb = skb_recv_datagram(sk, flags, noblock, &err); if (skb == NULL) goto out; data_skb = skb; #ifdef CONFIG_COMPAT_NETLINK_MESSAGES if (unlikely(skb_shinfo(skb)->frag_list)) { /* * If this skb has a frag_list, then here that means that we * will have to use the frag_list skb's data for compat tasks * and the regular skb's data for normal (non-compat) tasks. * * If we need to send the compat skb, assign it to the * 'data_skb' variable so that it will be used below for data * copying. We keep 'skb' for everything else, including * freeing both later. */ if (flags & MSG_CMSG_COMPAT) data_skb = skb_shinfo(skb)->frag_list; } #endif /* Record the max length of recvmsg() calls for future allocations */ nlk->max_recvmsg_len = max(nlk->max_recvmsg_len, len);//更新了最长的的接收数据长度 nlk->max_recvmsg_len = min_t(size_t, nlk->max_recvmsg_len, SKB_WITH_OVERHEAD(32768)); copied = data_skb->len; if (len < copied) {////判断如果获取到的skb数据长度大于大于本次接收缓存的最大长度,则设置MSG_TRUNC标识,并将本次需要接收数据量设置为接收缓存的长度 msg->msg_flags |= MSG_TRUNC; copied = len; } skb_reset_transport_header(data_skb); err = skb_copy_datagram_msg(data_skb, 0, msg, copied);//将skb中的实际数据拷贝到msg消息中---------- 详解3 if (msg->msg_name) { DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);//拷贝完成后这里开始初始化地址结构 addr->nl_family = AF_NETLINK;//将family这是为AF_NETLINK地址族 addr->nl_pad = 0; addr->nl_pid = NETLINK_CB(skb).portid;//设置portid号为保存在原端skb扩展cb字段中的portid,对于这里接收内核发送的skb消息来说本字段为0 addr->nl_groups = netlink_group_mask(NETLINK_CB(skb).dst_group);//设置组播地址,----------- 详解4 msg->msg_namelen = sizeof(*addr); } if (nlk->flags & NETLINK_F_RECV_PKTINFO)//如果设置了NETLINK_RECV_PKTINFO标识则将辅助消息头拷贝到用户空间 netlink_cmsg_recv_pktinfo(msg, skb); if (nlk->flags & NETLINK_F_LISTEN_ALL_NSID) netlink_cmsg_listen_all_nsid(sk, msg, skb); memset(&scm, 0, sizeof(scm)); scm.creds = *NETLINK_CREDS(skb); if (flags & MSG_TRUNC)//判断是否设置了MSG_TRUNC标识 copied = data_skb->len;//如果设置了就重新设置copied为本次取出的skb中获取数据的长度(特别注意!) skb_free_datagram(sk, skb);//释放skb消息包 if (nlk->cb_running && atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf / 2) { ret = netlink_dump(sk); if (ret) { sk->sk_err = -ret; sk->sk_error_report(sk); } } scm_recv(sock, msg, &scm, flags); out: netlink_rcv_wake(sk); return err ? : copied;//返回接收数据长度 } 3. skb_copy_datagram_msg 调用skb_copy_datagram_msg()函数将skb中的实际数据拷贝到msg消息中(这里进行了一次数据拷贝动作,将skb中的数据直接拷贝到msg指向的用户空间地址处)。 4. netlink_group_mask 在拷贝完成后这里开始初始化地址结构,这里将family这是为AF_NETLINK地址族,然后设置portid号为保存在原端skb扩展cb字段中的portid,对于这里接收内核发送的skb消息来说本字段为0,然后设置组播地址,该值在前文中内核调用nlmsg_multicast()发送组播消息时设置(对于单播来说就为0),netlink_group_mask()函数将组播地址的位号转换为实际的组播地址(mask),然后这是msg的地址长度为nl_addr的长度。

    2024-07-26 278浏览
  • 什么是mmap ?

    一、简介1. mmap 是什么?

    2024-07-19 286浏览
  • 基于EEPROM的简易类文件的数据读写库

    虽然EEPROM的读写速度较慢,但它具备一些Flash无法替代的独特特性,比如字节级别的读写操作。 因此,EEPROM仍然被广泛应用于许多电子设备中,且需求量依然很大。在此,向大家介绍一款轻量级且简易的EEPROM数据读写库:eepromfs。 关于eepromfs eepromfs 是一个基于 EEPROM 的简易类文件数据读写库,非常适合在进行动态功能增减时进行参数管理。 该库的特点是增加或删除参数块类似于操作文件,不会影响已有的数据存储。适用于 EEPROM 硬件资源充足的情况。 开源地址:https://gitee.com/wtau_zaozao/eepromfs 使用说明和软件架构 开源库使用说明: 该库适用于基于Arm处理器的系统,并支持例如24C256、24C512等容量较大的EEPROM存储芯片。 软件架构: 1. EEPROM存储区管理:在 EEPROM 存储区的开头存储eepromfs的管理信息。 2. 文件管理:采用链表方式串联每一个文件。 3. 文件创建:每个文件创建时需指定名称和大小。新增加的文件会自动追加到链表末尾。 提供各类格式化、读、写、删除的函数接口,方便用户直接调用: 开源地址(复制到浏览器打开): https://gitee.com/wtau_zaozao/eepromfs 文章来源于网络,版权归原作者所有,如有侵权,请联系删除。

    2024-07-05 244浏览
  • MCU串口如何自动识别波特率?过程讲解

    现在的单片机资源越来越丰富了,其中我们常用的串口也是内部集成了多个,关键功能也越来越强了。 我们有些应用可能会用到串口自动识别波特率,今天就来讲讲MCU串口自动识别波特率底层的常见的原理,以及MCU的案例。 自动识别波特率常见原理 串口自动识别波特率(Auto Baud Rate Detection,简称ABR)的底层原理主要基于串口通信中数据的传输特性和设备间的交互机制。 以下是几种常见的自动波特率识别原理。 起始位和停止位检测法:原理:串口通信中,每个数据包的开始都会有一个起始位(通常为0),结束有一个或多个停止位(通常为1)。通过检测这两个位的时间间隔,可以计算出波特率。特定模式检测法:原理:发送设备发送一系列具有特定模式的字节(如固定的字节序列或模式),接收设备尝试以不同的波特率接收并检测这些模式。当检测到与发送模式匹配的数据时,即可确定当前的波特率。波特率扫描法:原理:发送设备在初始化阶段发送一个包含多个不同波特率的扫描命令。接收设备在接收到这些命令后,会自动检测并匹配最接近的波特率。周期性信号检测法:原理:利用信号的周期性变化进行波特率检测。通过分析信号的频率和周期性特征,可以推算出波特率。 MCU硬件串口自动识别波特率 现在市面上很多新推出的MCU都有波特率自动识别的功能,这里以瑞萨RA系列单片机为例,配合e2 studio给大家讲讲串口自动识别波特率的配置。 UART1配置 UART1的配置只需要按照应用要求来做就可以,但必须留意所选的RXD脚必须跟IRQ是复用的,以便做软件切换。 由于是做波特率自适应,属性页面中关于Baud的配置可以忽略。 GPT配置 由于需要通过定时器来做start bit的时间间隔测量,可以选用GPT/AGT使用one-shot/Periodic模式并留足够Period值余量来确保start bit在低速下不会溢出。 P402管脚配置为IRQ4 将SCI1串口默认的(RXD)P402管脚改为IRQ4,并添加中断入口函数。 中断入口函数里面首先判断是否发生了下降沿触发,然后启动定时器,等再次中断进入后,停止定时器,并取得定时器计数值,通过跟默认已知的系统时钟参数相除,就能得到确切的波特率数值。 波特率计算和相关寄存器配置 取得波特率实际值后,通过硬件手册上的相关的方程式就能够反推出几个控制波特率的寄存器的配置需要值 (brr,  semr , cks等)。 并将相关计算出的数值修改到FSP SCI1的全局变量结构体内 (g_uart1_baud_setting)。 修改P402管脚配置为UART-RXD 然后将P402管脚修改为UART功能,并启动 UART_Open() 函数,设置波特率已配置标志。 这样主函数就能够直接通过已侦测到的波特率直接发送数据和开始接收数据。 上述方法只需通过底层寄存器配置就能使简单快速做波特率侦测、计算和配置。 通过这种方式,只要芯片系统时钟符合范围要求,任何非通用或者极高速/超低速的波特率也是能使实现侦测并配置使用。 来源:瑞萨嵌入式小百科

    2024-06-28 288浏览
正在努力加载更多...
广告