别被IIC总线给坑了~
大家好,我是bug菌~1、I2C通信理解很多朋友在进行IIC通信协议开发的时候比较迷茫,一方面是可能长时间没怎么用了,相关的知识有所忘却,也算正常,不过如果重新围着通信时序图看来看去,那还是比较麻烦的,比如IIC的起始电平条件、停止电平条件、以及数据保持即更新条件等等:虽然每个器件对IIC通信的波形要求不是太相同,但IIC的通信时序容忍度非常高,基本上常规范围的通信参数和驱动都是通用的。IIC数据的传输过程,在SCL通信同步时钟节拍的控制下,主机发送数据主动使得总线电平发生变化,供从机检测接收,而当从机接收到数据以后接着主动的拉低SDA来作为应答信号通知主机,如果没有拉低则表示非应答。之前很多朋友都不太理解IIC的主机是怎么检测到从机应答的,似乎从SDA线上的波形看都好像是主机发送出来的,所以在进行通信波形解析的时候一定要注意区分信号到底是主机还是从机在处理。2、IIC数据帧对于通信的应用,重要的并不是所谓的电平变化,当然也不是说不重要,毕竟有时候通信不稳定还得从原始波形进行分析,但是大部分应用开发人员更多的是要了解如何传递数据帧,掌握好数据帧的传递过程和方式。不同厂商的数据帧稍微有所差异,比如7位地址、8位地址和10位地址,但总体上都是大同小异,大家可以参考对应的芯片手册进行学习,这里以最常用的7地址位跟大家介绍一下:IIC是一种主从通信方式,通信发起者为主机,主要熟悉三种数据帧传递过程:1、单次或连续向从机写数据注意如上仅仅只是数据帧传递,类似于我们平时的串口通信,而至于通信数据域内的数据含义,是由通信双方共同约定即可,也就是所谓的应用层协议的制定了。2、单次或连续向从机读数据读数据的过程主机发送的读写标志位发生变化,在数据部分从机主动控制总线发送数据给主机,然后主机来进行应答,刚好与IIC写数据相反。3、通讯过程读写切换在通信过程中需要进行读写切换时不需要发送停止,而是应答以后重新发一次起始和从机地址及读写状态,接着进行下面的数据处理即可。3、IIC通信别忘了上拉对于IIC总线不要忘记通信IO上拉,上拉主要是保证信号线在空闲的状态保持高电平,也就是逻辑1,。同时IIC总线采用的是一种开漏输出的架构,通信线上的器件可以将线路的电平拉低,即逻辑0;但是无法主动将线路拉高到逻辑1,所以上拉必不可少。所以为了确保通信线上能够提供足够的驱动能力,同时也不能导致信号失真,上拉电阻阻值的选择尤为重要。4、上拉电阻怎么选?上拉电阻该怎么选呢?那影响因素可就多了~1、通信的总线长度通常通信线路越长,电阻要稍微大一点。2、通信的总线材质如果总线提供的容性负载较高,要适当减小电阻,以加快信号的变化时间。3、通信的速率适当降低上拉电阻,提高驱动电流,加快电平反应速度。具体情况就具体分析和折中去选择上拉电阻了,最后就是注意电平上的匹配,避免损坏芯片~
最后一个bug
3 19 硬创社
C语言把结构体玩活了~
今天主要是跟大家详细聊聊container_of这个宏定义,非常经典的宏,只是一直没有抽时间细细品味,今天就跟大家一起来看看有何神奇之处:1、offsetof首先我们需要简单看看offsetof(TYPE, MEMBER) 这个宏定义,它是用于计算一个结构体中某个成员的偏移量。其第一个参数 TYPE 是一个结构体类型,第二个参数 MEMBER 是 TYPE 中的一个成员变量名。它将返回类型为 size_t 的整数,表示 MEMBER 相对于 TYPE 起始地址的偏移量。基本原理是根据 C 语言的数据对齐机制,成员变量在类型定义中的相对位置决定了它的偏移量。#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 该宏定义使用了C语言中的指针运算和类型转换。具体实现步骤如下:1、(TYPE *)0:将0强制类型转换为指向类型为TYPE的指针,得到了一个结构体TYPE的空指针。2、&((TYPE *)0)->MEMBER:求出结构体类型TYPE中成员MEMBER的地址。其巧妙之处在于,由于空指针不指向任何对象,因此这个成员的地址就是相对于结构体首地址的偏移量。3、(size_t):将偏移量转换为无符号整型数,以满足C语言标准库中对offsetof()返回值的类型要求。该宏定义可以在编译时就直接计算出偏移量,避免了运行时的计算开销,因此比通过变量名访问成员的方式更为高效,通常用在需要直接访问结构体成员的底层代码中,例如在操作系统内核、嵌入式系统以及一些高性能计算应用中。struct TestStruct {       int value1;       char value2;       double value3;   };   size_t offset = offsetof(struct TestStruct, value2);   如上例,offset 变量将会存储 value2 相对于 TestStruct 起始地址的偏移量。在这种情况下,因为 TestStruct 中的 value1 通常占用了 4 个字节,value2 占用了 1 个字节,所以 value2 相对于结构体起始地址的偏移量应该是 4。2、container_of讲完offsetof,来到今天的主角container_of,container_of()是一个在linux内核中经常使用的宏,用于获取一个结构体成员指针所在它所属的结构体的指针,有点绕口,细细品味。该宏包括也主要包括三个参数:ptr:结构体中某个成员的指针;type:结构体类型名称;member:结构体中ptr指向的成员名称。首先,宏container_of()确定了ptr指向的成员在结构体中的偏移(offset)。通过offsetof()宏就可以得到这个偏移,其参数为结构体类型和成员名称。得到偏移后,再通过减去偏移的方式得到指向整个结构体的指针,巧妙吧。具体实现如下:#define container_of(ptr, type, member) ({ \           const typeof(((type *)0)->member) *__mptr = (ptr); \           (type *)((char *)__mptr - offsetof(type, member)); }) 其中,typeof是GCC的一个扩展关键字,用于返回一个表达式的类型,可惜,大部分非GCC编译器不一定能支持。假设ptr指向的成员变量的类型为T,__mptr就是一个指向T类型的指针。然后,调用offsetof()即可得到member在type类型中的偏移量,最后返回一个指向type类型的指针。注意,尖括号不能省略,因为它表示类型转换。此外,container_of()宏使用了一个GCC的语言扩展"statement expression",即后面的{},可以在其中包含多条语句。下面给出一个示例,用于说明container_of()的使用方法:#include [removed]#include [removed] #define container_of(ptr, type, member) ({ \           const typeof(((type *)0)->member) *__mptr = (ptr); \           (type *)((char *)__mptr - offsetof(type, member)); }) struct student {     int id;     char name[20]; }; int main() {     struct student stu = {10001, "Zhang San"};     char *pname = stu.name;     struct student *pstu = container_of(pname, struct student, name);     printf("ID: %d, Name: %s\n", pstu->id, pstu->name);     return 0; } 如上例,pname指向stu的name成员,通过container_of()宏获得了指向整个struct student结构体的指针pstu,然后就可以访问id和name成员了。
最后一个bug
5 12 开源硬件平台
这个变量要不要用volatile修饰呢?
大家好,又见面了,我是bug菌~在嵌入式软件开发过程中,如果对volatile不熟,那可以你应该是个"假嵌入式程序员",因为一个变量需不需要使用volatile考虑的场景挺多的,如果在某些场景下乱用,会影响程序运行效率,有时候忘记加甚至会使得程序发生异常,那么bug菌今天就大家好好聊聊这个C语言关键字:1、传统定义volatile直译为“易变的”,也就是告诉编译器这个变量随时都可能发生变化,编译器你跟我“特殊照顾一下“。那么编译器通常会怎么去处理使用volatile修饰的变量呢?对于C变量都是代表着对应的内存,读取使用volatile修饰的变量,会直接从其所对应的内存地址中获取最新的数据,否则,编译器会对其访问进行优化,比如直接从缓存中读取副本、或者是从寄存器中读取副本。这样就可能会导致数据更新不一致的问题。2、最常用的地方从前面对volatile的功能描述,我们可以知道volatile最常用于那些与硬件外设寄存器打交道的地址,这样确保每次对寄存器的读取都是从内存中获取的最新值,比如:再比如下图所示,如果我们向地址0x812100地址连续改变其值:那么编译器通常会将其直接优化为第三条操作,并不会去执行前两条操作,这样会造成写寄存器时序上的问题。如果采用volatile去修饰,则三条命令便会依次执行,达到我们代码所示三次操作的目的。3、更复杂一点的,也是最重要的 其实对于volatile所解决的问题用更加专业的说法可以分为:可见性和有序性。1、可见性所谓可见性,通常是在多线程访问共享数据的情况,当一个线程对共享变量进行修改,而其他线程能否立即观察到这个修改的性质。在我们目前大部分单核一级缓存的CPU无需考虑这个问题,而对于现场多核多级缓存处理器,各个现场都会维护着自己的缓存,如果仅仅只是更新到了自己的缓存中那么其他线程是无法立马感受到这个修改的,最终导致结果不一致。2、有序性很多时候也叫作重排序,说白了就是对执行指令进行了执行顺序上的优化,以不改变指令运行的最终结果,而改变指令的执行顺序。编译器可以调整指令,同样处理器的多级流水线和乱序执行也同样可以改变指令执行顺序;甚至为了多级缓存的高效执行,也同样会对内存读写操作进行重排序。然而这样的重排序,却会对多线程并发访问共享数据的过程中产生问题,从而不符合我们编程源码的预期执行顺序。但对于volatile只能在一定程度上防止指令重排序,其只能保证单个变量访问的有序性,而不能保证整个程序的有序性,所以这一点是大家尤为要注意的。所以讲了这么多,相信以后大家再开发中也都会遇到。
最后一个bug
1 7 立创开发板
四款主流的轻量级嵌入式网络协议栈
在嵌入式开发软件中网络协议栈实在是太重要了,可以说现在凡是被称为智能的设备,几乎都需要具备联网的功能。然而让自己手上的平台具有联网的功能,基本上都会要选择一款软件网络协议栈,当然啦用硬件协议栈也挺多的,不多相对来说功能比较容易受限。而软件协议栈徒手写的话,可以说对于大部分普通开发者而言是不太现实的。毕竟成熟的开源的网络协议栈挺多的,重复造轮子其实意义并不大。那么今天bug菌跟大家简单介绍一下四款嵌入式中应用比较广泛的网络协议栈。1、LWIPlwIP 是一个非常流行的开源 TCP/IP 协议栈,最初是在瑞典计算机科学研究所的计算机和网络架构实验室联合开发,它专门为嵌入式系统设计,具有低内存占用和高效率的特点。lwIP是TCP/IP协议的一个小型独立实现,重点是减少RAM的使用,同时仍然具有全规模的TCP。这使得lwIP适用于具有数十千字节空闲RAM和大约40千字节代码ROM空间的嵌入式系统。同时其具有TCP、UDP、IP、ICMP、ARP、DNS、SNMP、DHCP等协议的支持,并且易于移植到各种操作系统和处理器体系结构上。目前在非常多的物联网模块或者嵌入式操作系统重都有广泛的应用。2、uIPuIP协议栈是专为8/16位的嵌入式微处理器设计的小型TCP/IP协议栈。去掉了TCP/IP一些不常用的功能,采用BSD授权,遵循RFC标准,完全由C语言编写。它以库函数的形式提供给嵌入式 Internet 应用开发人员,并采用了一种基于事件驱动的程序模型(说白了就是不断地去轮询),并且还不使用动态内存,都是共用同一个缓存区,基本上不存在数据的copy,从而大大减少了代码容量和 RAM 的占用量,在单片机中Flash和RAM都占用比较小。可以说,在51单片机上运行也很丝滑。3FreeRTOS-Plus-TCP适用于 FreeRTOS 的开源、可扩展和线程安全 TCP/IP 堆栈。它提供了一个熟悉的基于标准 Berkeley 套接字的接口, 简单易用,便于快速学习。 高级用户还可以使用替代回调接口。功能和RAM占用空间完全可扩展,使FreeRTOS-Plus-TCP 既适用于较小的低吞吐量微控制器, 也适用于较大的高吞吐量 微处理器。4、RL_TCP netRL-TCPnet 组件来自于 RL-ARM 库,而RL-RAM又是Keil MDK自来的实时运行库,RL-TCPnet 是一个TCP/IP 协议协议栈。该堆栈旨在减少内存使用量和代码大小。这使得它适用于资源有限的嵌入式系统设备。RL-TCPnet 库是ARM7、ARM9、Cortex-M3等软件架构的底层思实现软件。用户应用程序使用标准 C 结构编写,并且使用 ARM 编译器编译,并且其中已经集成了web服务器、SMTP发客户端、SNMP Agent、DNS解析等高层应用,且稳定性还是挺不错的。
最后一个bug
3 9 开源硬件平台
嵌入式实时性可以考虑静态链表~
首先跟大家聊聊什么是静态链表,静态链表是一种使用数组来实现的链​表结构。在静态链表中,数组的每个元素称为一个节点,节点中包含两部分信息:数据和指向下一个节点的“指针”,这里的指针并不是C语言里语法上的指针,它主要是标记节点在数组中的位置,也就是数组的下标索引,其实广义上也是一种指针吧。再来看下静态链表怎么玩的吧~所以与动态链表的差异点,主要是静态链表的节点在内存中是连续存储的,而且节点的数量是固定的。有代码有真相:#include [removed]#define MAX_SIZE 100 // 静态链表的节点结构 typedef struct Node {     int data; // 数据域     int next; // 指针域,指向下一个节点的索引 } Node; // 初始化静态链表 void init(Node list[]) {     // 将所有节点的 next 域初始化为 -1,表示空闲状态     for (int i = 0; i < MAX_SIZE; i++) {         list[i].next = -1;     } } // 释放节点 void release(Node list[], int index) {     // 将节点标记为可用状态     list[index].next = -1; } // 获取可用的空闲节点索引 int getFreeNode(Node list[]) {     for (int i = 0; i [removed]
最后一个bug
1 2 立创开发板
Keil中三种手动结构体对齐方式,别用错了~
最近移植了一些开源组件,发现较多的语法跟编译器相关,如果没有跨平台处理,确实大大降低了程序的可移植性,其中尤为突出的就是结构体字节对齐属性的标识,通常编译器采用默认字节对齐方式,按照处理器架构的要求来决定的。比如如下结构体在stm32中默认为4字节对齐:  typedef struct _tag_Test1   {      uint8_t member1;      uint16_t member2;   }sTest1;  stSize = sizeof(sTest1); 自然sizeof获得的结构体大小也是4。然而默认对齐方式有时候并不满足我们编程的需求,比如需要降低一些内存占用,或者提高相关数据的访问效率等等,我们会手动的声明相关变量的对齐方式。那么这里总结了下AC5编译器进行字节对齐的几种方式:1、#pragmapack#pragma pack 是一个编译指令,用于指定结构体、联合体和类成员的字节对齐方式。在 Keil uVision5 中,可以使用 #pragma pack 指令来设置字节对齐方式。一般我们用如下方式标识#pragma pack(n)其中,n 是对齐系数,表示按照 n 字节对齐。常见的对齐系数包括 1、2、4、8 等。例如,若要将对齐系数设置为 4,比如:#pragma pack(4),该指令通常放置在结构体、联合体或类的定义之前,以影响其后的所有定义,这里尤其需要注意,很多时候忘记恢复字节对齐导致了一些没必要的问题。这样一来,所有在 #pragma pack 后声明的结构体、联合体或类成员都将按照指定的字节对齐方式进行排列。那么如果我们需要取消则需要采用#pragmapack() 来取消结构体对齐。#pragma pack (1) typedef struct _tag_Test2 {     uint8_t member1;     uint16_t member2; }sTest2; #pragma pack () // 取消结构体对齐 stSize = sizeof(sTest2); 通过这样的定义,使得sTest2结构体整体大小只占用3个字节在,这种方式在MDK中比较常用。当然有经验的朋友该说了,我用#pragma pack(push,n)比较多,没错,该语法也同样是可以的,比如例子:    //#pragma pack (1)     #pragma pack(push,1)      typedef struct _tag_Test2     {         uint8_t member1;         uint16_t member2;     }sTest2;     //#pragma pack () // 取消结构体对齐     #pragma pack(pop)     stSize = sizeof(sTest2); 既然都聊到这个份上了,该谈谈他们的差异了:#pragmapack(n)和#pragma pack(push, n) 其实在功能上没太大的区别,仅仅只是在使用方面略有不同。#pragma pack(n):这个指令直接设置当前字节对齐系数为 n。这意味着在此指令之后声明的结构体、联合体或类成员都将按照指定的字节对齐方式进行排列。每次使用 #pragma pack(n) 时,都会覆盖之前的对齐设置,因此它可能会影响后续的代码。没有保存当前的对齐方式,因此在使用完之后,如果需要还原到先前的对齐方式,就需要手动重新设置。#pragma pack(push, n):这个指令其实也是设置当前字节对齐系数为 n。但是它还有一个功能,就是将当前的对齐方式保存到编译器的栈中。这意味着,使用 #pragma pack(push, n) 后,可以在代码的后续部分使用 #pragma pack(pop) 来恢复之前保存的对齐方式,而不会受到之后代码中 #pragmapack指令的影响。因此,#pragma pack(push, n) 更灵活,可以避免在代码的后续部分不小心修改了对齐方式而导致错误。2__attribute__((__packed__))__attribute__((__packed__)) 是 GCC 和一些兼容 GCC 的编译器(如 Clang)提供的一个特性,用于指示编译器以紧凑的方式存储结构体或类,即取消对齐。使用案例如下:    typedef  struct __attribute__ ((__packed__)) _tag_Test3      {         uint8_t member1;         uint16_t member2;     }sTest3;      stSize = sizeof(sTest3); 那么最终结构体的大小也将是3个字节。3、干脆的__packed相信有经验的各位都比较喜欢使用这个属性吧:    typedef __packed struct _tag_Test4      {         uint8_t member1;         uint16_t member2;     }sTest4;    ;     stSize = sizeof(sTest4); __packed 是一种特性,指示编译器取消对其成员的自然对齐。它的作用类似于 __attribute__((__packed__)),但是它更加与平台无关,因为它是一种更通用的约定,不依赖于特定编译器。虽然说__packed 是一种常见的约定,但它并非标准 C 语言的一部分,因此在不同的编译器和平台上可能具有不同的行为,使用时应谨慎考虑平台兼容性和性能问题。
最后一个bug
0 7 硬创社
MCU C语言开发__attribute__((aligned(n))) packed
为什么使用__attribute__((aligned(1)))进行属性声明的结构体大小不能达到__attribute__((packed))的效果,然后跟他聊了小一会,那么今天就以此文再总结总结。1、默认对齐其实所谓的对齐,主要是包括两个内容,数据地址的对齐与数据结构的填充,数据地址的对齐主要是方便CPU的访问,然而为了完成数据地址对齐,对于结构体数据需要插入一些无意义的数据,我们也叫数据填充。在没有手动指定对齐方式的时候,编译器通常会进行默认自动对齐,像STM32默认采用的是自然对齐方式。在自然对齐方式下,数据类型的起始地址必须是其大小的整数倍。例如,一个四字节(32位)的整数必须从一个地址处开始,这个地址是4的倍数,都是为了提高内存访问效率。在许多存储器系统中,以4字节为单位进行访问速度更快,因为它与内存总线的宽度相匹配。这样可以减少读取和写入操作的次数,提高数据传输速率,从而提高系统性能。2、对比__attribute__((aligned(n))其实有很多种用法,而且其放在什么位置修饰什么内容也会产生不同的效果,最常用的就是直接修饰变量,使得变量的地址对齐到设置的对齐个数上来。比如:    typedef  struct  _tag_Test1      {         uint8_t  member1;         uint32_t member2;         uint8_t  member3;     }__attribute__((aligned(16))) sTest1 ;     Size = sizeof(sTest1); 此时aligned修饰的是结构体类型,此时在32位系统中16字节对齐,此时该结构体占用16个字节。然后我们来看如下位置:    typedef  struct  _tag_Test1      {         uint8_t  member1;         uint32_t member2;         uint8_t  member3;     } sTest1 __attribute__((aligned(16)));     static sTest1 test;     Size = sizeof(sTest1); 此时aligned修饰的是具体的变量,并不会改变结构体的内部成员的对齐方式,仅仅只是改变结构体所定义的变量地址对齐方式。而且使用__attribute__((aligned(n))进行对齐声明,编译器通常会将所声明的对齐方式n与编译器默认的对齐方式进行比较,取最大值来进行对齐处理,所以这就是很多朋友常提到的,__attribute__((aligned(n))在对结构体进行修饰的时候结构体大小只会大不会小。然而__attribute__((packed))所表述的含义则不同了,它则是取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,也就是常说的采用1字节对齐的一种紧凑的对齐方式。所以__attribute__((aligned(1))和__attribute__((packed))会得到不同的效果,__attribute__((aligned(1))通常会采用系统默认的对齐方式,而__attribute__((packed))则会采用紧凑的1字节对齐方式。3、注意__attribute__((packed))会让结构体以紧凑的方式进行排列,同样 #pragma pack (1)也会起到相同的效果,而__attribute__((aligned(n))) 实际上只影响紧随其后的变量或者结构体的对齐方式,而不会影响结构体内其他成员的对齐方式,当然编译器将会调整结构体的对齐方式,从而可能在结构体内部添加填充字节,以满足字节对齐的要求。即使在结构体中某个成员使用了 __attribute__((aligned(n))),其他成员的对齐方式仍然由编译器的默认规则决定。当然如果真的有需要对结构体内部程序进行指定地址对齐,可以使用如下操作,给内部成员对齐单独指定。    typedef  struct _tag_Test1      {         uint8_t member1;         uint16_t  __attribute__((aligned(8))) member2;     }sTest1 ; 那么此时member2地址会落在8字节地址对齐处,member1到member2之间的多余内存会被填充,结构体大小也会发生变化。
最后一个bug
0 6 开源硬件平台
嵌入式系统如何尽可能避免存储数据丢失与损坏?
大家好,我是bug菌~对于一些需要动态存储数据的嵌入式系统往往我们需要考虑系统在各种状态的数据可靠性问题。当然也不仅仅这些数据敏感的协议,最常见的就是你向存储系统写入数据的过程中给断电了,系统下一次上电跑飞了~掉电过程是最为敏感的情景,也是一般在系统设计前期要重点考虑的,那么今天bug菌就跟大家重点聊聊一般的嵌入式系统如何尽可能的避免重要存储数据的丢失与损坏。1、掉电检测  前面也提到了,掉电过程是数据丢失和损坏比较高发的状态,一方面离不开硬件上掉电备电电源的相对稳定性和持久性,另一方面也需要软件部分最好掉电过程系统完整的收尾工作,最常见的问题就是正在掉电,你还在使劲的写文件或者其他改变存储介质的操作,运气好可能只是文件写少了;运气不好直接文件系统就崩溃了~那么快速的掉电检可以帮助系统在断电前尽早将这些数据进行保存,以确保系统重新上电后能够恢复到正常工作状态,而不会因为掉电导致数据丢失或损坏。2、存储器件的寿命与稳定性电子产品都有使用寿命,在嵌入式设备里面常用闪存存储器,即Flash,而闪存通常以擦除/写入循环次数(P/E cycles)来衡量其寿命。常见的闪存产品如NAND和NOR闪存都有固定的P/E周期数量,一般在几千到几十万次之间,所以如果频繁擦写就会导致损坏,最终也会使得数据丢失,另外,闪存的寿命还受到温度、电压以及擦除/写入操作的影响。所以为了减少存储介质上的数据丢失要么选择高品质且可靠的存储介质,要么根据介质的特点优化存储算法,延长使用寿命。那么通常在软件层面有如下几种软件处理方法和策略:磨损均衡在闪存中,频繁写入相同的块会导致这些块的寿命提前耗尽,从而降低整个存储器的寿命。磨损均衡算法旨在平衡闪存中不同块的使用次数,避免某些块过早失效。可以通过选择写入次数最少的块来进行新数据的写入,或者通过重新映射块来实现。垃圾回收当删除或更新数据时,闪存中会产生垃圾数据,占用空间而无法直接写入。垃圾回收算法会定期检查闪存中的垃圾数据,并将其重新组织以释放可用空间。有效的垃圾回收算法可以减少擦除操作的频率,从而延长闪存的寿命,当然如果你没有用文件系统,只是裸写,基本上都是按顺序去写了。坏块管理坏块管理指的是处理闪存存储器中出现的无法正常读取或写入数据的坏块的过程。通过坏块检测、标记和替换,系统可以有效地识别和处理坏块,确保数据的完整性和可靠性。坏块管理还包括维护坏块映射表,以记录坏块的位置和替代块的使用情况。有效的坏块管理可以延长闪存存储器的寿命,提高系统的可靠性,并确保数据安全。写入放大减少写入放大指的是实际写入闪存的数据量与应用程序请求的数据量之间的差异。减少写入放大可以减少对闪存的写入操作,从而延长其寿命。这可以通过合并小的写入请求、延迟写入、以及数据压缩等技术来实现。静态和动态数据分离将静态数据(很少修改的数据)与动态数据(频繁修改的数据)分开存储在不同的闪存块中。这样可以避免频繁写入对静态数据块的影响,延长其寿命。温度和电压管理通过一些辅助的采样。来调节读写负荷,维持在合适的工作温度和电压可以减少对闪存的损坏和老化,从而延长其寿命。3、数据备份对于数据动态存储非常严格的应用需求场合,保证嵌入式设备的实时数据存储稳定性是非常重要的,特别是对于需要高可靠性和实时性的应用场景。以下是一些办法来确保嵌入式设备的实时数据存储稳定性:实时数据备份实时将数据备份到多个分区或者其他位置,例如本地存储和远程服务器,即是一块区域物理上遭到破坏,也能从其他区域进行恢复,极大的降低了数据丢失或损坏的概率。使用事务性存储机制采用具有事务性支持的存储机制,确保数据的原子性操作,即要么全部写入成功,要么全部失败,以避免数据不一致性,以免存在第三种状态完成系统的混乱与破坏。实时监控和错误处理建立实时监控系统来检测存储设备的健康状况,及时发现并处理存储设备的故障或错误,以前bug菌就接手到一些项目,写数据出了问题,好几天系统也没有提示,客户也没有及时查看,等发现问题已经好几周的数据异常了。采用更加成熟的文件系统一些支持掉电保护的实务型文件系统基本都支持日志功能或者文件系统级的保护机制。数据完整性校验实施数据完整性校验机制,例如循环冗余校验(CRC)或者哈希校验,来检测存储数据的完整性,及时发现和纠正数据损坏。
最后一个bug
0 5 立创开发板
有限元状态机的三种C语言实现方式
1、状态用 switch—case 组织起来, 将事件也用switch—case 组织起来, 然后让其中一个 switch—case 整体插入到另一个 switch—case 的每一个 case 项中 。 2、表格驱动法的实质就是将状态和事件之间的关系固化到一张二维表格里, 把事件当做纵轴,把状态当做横轴,交点[Sn , Em]则是系统在 Sn 状态下对事件 Em 的响应 。 3、它的实质就是把动作封装函数的函数地址作为状态来看待。
最后一个bug
0 7 开源硬件平台
老显示器该换了,否则会摧毁你的眼睛~
今天给大家带来一篇关于程序员护眼的文章,大部分伙计都是敲代码的,即使不是码农,也多半每天要抱着电脑处理各种事务,那么对眼睛来说还是挺大负担的特别眼睛原本就不好的朋友更加不友好,那么今天bug菌就大致聊聊显示器是如何摧残我们眼睛的,知己知彼才能减少伤害。1、太阳VS显示器 显示器是一种发光的电子设备,相对于眼睛而言,它就是一种光源,然而我们都知道自然界最大光源当属太阳了,太阳光是自然光包含了各种波长的光线,形成了我们所看到的色彩和亮度。而显示器的光则是由LED或LCD等发光体发出的人工光,它只包含了特定的波长和强度。太阳的光谱首先跟大家科普下蓝光、绿光、黄光和红光,他们是可见光谱中的四种主要颜色,它们具有不同的波长。蓝光的波长范围大约在380到500纳米之间,它具有较短的波长和较高的能量,因此显得更为明亮和刺眼。绿光的波长范围大约在500到565纳米之间,它处于可见光谱的中间位置,被认为是人眼最敏感的颜色。黄光的波长范围大约在565到590纳米之间,它具有较长的波长和较低的能量,在自然界中常见于日落时的太阳光。红光的波长范围大约在620到750纳米之间,它具有最长的波长和最低的能量,因此显得比较暗淡。这些颜色的波长范围是近似的,但确切的数值可能会在不同的资料和研究中略有差异。从生物学的角度,我们是需要这样比例的光线的。即使是太阳发出紫外线,也不全是对我们的伤害,紫外线可以促进人体皮肤中的维生素D的合成,当然凡事还是不能过度,物极必反。不同光源的频谱图太阳发出的光类似于第一个图形,而我们的显示器发出的光类似于第三个图形。从上图我们可以注意下 LED 灯发出多少蓝光和多少绿光。问题实际上不在于有多少蓝光,问题在于它们之间的比例,从上图中看我们的显示器几乎缺少红光的波长。我们可以通过降低屏幕的色温来粗略的解决这个问题,但红光永远不会变多,这就是为什么我们需要更多的红光,值得注意的是显示器中的红光、绿光和蓝光,并不完全等同于自然光中的红光、绿光和蓝光。如果你读到这里,红光实际上是最重要的光之一。科学已经证明,当植物获得大量红光和少量蓝光时,它们会长得更大。在人类看来,蓝光能够调节我们的情绪,这也很重要,但在晚上,它会刺激我们眼睛,导致褪黑激素分泌停止,如果我们周围存在大量蓝光,我们就无法入睡,这也是为什么很多人玩手机后无法睡眠的原因之一吧。标准的爱迪生白炽灯泡擅长产生蓝光,但在早上我们需要蓝光来提升情绪,这刚好这种影响与作用完全相反了。所以太阳光,它不仅是全光谱光,而且在白天和黑夜也会发生变化,我们同样也需要这种变化,因为太阳光一直伴随着我们的进化。2、灯VS显示器 显示器与灯比较相似,它是由非常多小灯组合在一起形成的电子产品,然而这些发光的电子产品基本上都会闪烁,只是我们人眼的大脑反应是感觉不到的,这主要是为了减少它们的能源使用和亮度的调节,你可以通过拍摄一些灯管的视频就会有明显的感受。虽然我们的大脑感受不到这种伸缩,我们的眼睛却对此非常的敏感,瞳孔会控制眼睛的进光亮,在黑暗中,我们需要更多的光线,而瞳孔是放大的。当我们周围有很多光线或有很多日光时,瞳孔会缩小。这样我们的眼睛就处于一个频繁收缩的状态,时间久了就容易累。可能很多朋友该问了为什么LED不能一直亮着呢?我们做嵌入式的朋友“呼吸灯”的玩法应该是非常熟悉了,通过PWM来调节LED的亮度,方便调节亮度这是它一个重要的应用,其实还有另外一方面就是能够节约能源,能效比较高,这样相同的电池或者电量能够用得更久,对于笔记本这样的移动设备的续航买点又是一大提升,所以这也是制造商更喜欢制造低背光频率显示器的原因吧。其实目前一些显示器所谓的没有闪缩,其实只是闪烁的频率更高,这对眼睛来说也算是一件好事吧,所以有个小技巧通常显示器的亮度越亮,背光的频率就越高。3、最后的救赎1、使用好一点的显示器,一般都有护眼模式,减少蓝光;还有一些无闪烁显示器,这种显示器内部使用一种称为直流调光的东西,它不会使背光闪烁,但亮度降低范围有限。2、使用一些护眼软件,通过使用显卡来降低屏幕的亮度。这会稍微改变颜色,但对于不需要屏幕精确颜色的人来说,这是便宜而有效的解决方案。3、定期休息,这基本上是每个眼科医生都会跟你说得,使用20-20-20法则,即每20分钟看离开屏幕20英尺(约6米)远的地方,持续20秒钟。如果您还有更好的护眼技巧,欢迎大家留言补充,拯救大家的眼睛~
最后一个bug
8 14 硬创社
写大型C语言工程makefile构建
最开始学习linux应用开发编写的时候,估计大部分伙伴们都是在一个目录里面编译整个工程,主要是linux通常没有非常合适的集成开发环境。以前单目录的方式实在太过捡漏,在linux环境中进行C代码工程开发很多时候需要编写一个相对比较通用的makefile,一劳永逸,能自动查找并归类每个目录的文件进行编译。可能很多朋友会选择一些cmake,scons等自动化构建工具,但也有部分伙计编写makefile也完全够用,嵌入式平台迭代速度不快的话基本上可以成为传承级代码,那么今天大致梳理了一下makefile中构建大型一点的工程需要用到的一些编译语法与函数。1、常用的特殊变量这些符号是Makefile中的特殊变量,用于在规则中引用文件名和目标,$^ 用于表示所有的依赖文件列表,多个文件以空格分隔。在规则中,它可以用来引用所有依赖文件的列表。例如:target: dependency1 dependency2 dependency3 command $^ 在这个例子中,$^ 将会展开为 dependency1 dependency2 dependency3。$@ 用于表示目标文件的名称。在规则中,它可以用来引用目标文件的名称。例如:target: dependency1 dependency2 dependency3 command $@ 在这个例子中,$@ 将会展开为 target。$<用于表示规则中的第一个依赖文件。通常在单目标多源文件的情况下使用。例如:target: dependency1 dependency2 command $< 在这个例子中,$< 将会展开为 dependency1。$? 用于表示所有比目标文件新的依赖文件列表。通常在需要重新生成目标文件的情况下使用。例如:target: dependency1 dependency2 command $? 在这个例子中,如果 dependency1 比 dependency2 新,则 $? 将会展开为 dependency1;如果 dependency2 比 dependency1 新,则 $? 将会展开为 dependency2;如果两者都是相同的时间戳,则 $? 将为空。$* 用于表示规则中目标文件的文件名部分(不包括扩展名)。通常在需要将目标文件名作为参数传递给命令时使用。例如:%.o: %.c gcc -c $< -o $@ ./process $* 在这个例子中,如果目标文件是 example.o,则 $* 将会展开为 example。除了上面提到的 $^、$@、$<、$? 和 $*,Makefile 中还有一些其他常用的特殊变量。$(MAKE): 用于表示 make 命令的名称。这在递归调用 make 的时候非常有用。$(CC): 用于表示 C 编译器的名称,默认情况下是 cc。你可以在 Makefile 中使用这个变量来指定编译器。$(CFLAGS): 用于表示传递给编译器的参数。这个变量通常用于指定编译选项,比如警告选项、优化选项等。$(LDFLAGS): 用于表示传递给链接器的参数。这个变量通常用于指定链接选项,比如库路径、库文件等。$(RM): 用于表示删除文件的命令,默认情况下是 rm -f。你可以在 Makefile 中使用这个变量来指定删除文件的命令。比如$(CFLAGS) 是一个在 Makefile 中常用的特殊变量,用于指定传递给编译器的参数,可以使用它来设置编译选项,比如警告选项、优化选项等。通常是在 Makefile 中定义 $(CFLAGS) 变量,并在编译规则中使用。例如:CC = gcc CFLAGS = -Wall -O2 # 编译规则 %.o: %.c $(CC) $(CFLAGS) -c $[removed]
最后一个bug
2 4 开源硬件平台
​​了解USB PD快充协议中的受电芯片和​​供电芯片
受电芯片:位于手机、电脑等受电设备中,负责通过USB Type-C接口与充电器通信,协商需要的电压/电流(如请求20V快充),并管理电力输入的安全(如过压保护)。 供电芯片:位于充电器或移动电源等供电设备中,主动告知自身支持的供电能力(如5V/9V/20V),根据受电端请求动态调整输出功率,并确保输出安全(如过流保护)。 协同流程: 连接后,供电芯片先“广播”可提供的电压选项; 受电芯片根据设备需求选择最佳方案并发送请求; 供电芯片切换电压,双方确认后开始高效充电
最后一个bug
2 8 硬创社
大部分公司的嵌入式软件只有debug版本
大部分开发工具或者IDE中都为你创建的工程配置为debug和release两个版本,字面上很好理解,一个用于调试debug,一个用于发布release,那么是不是很多伙计都是debug版本用到老?那么今天聊聊release的必要性吧。1、两个版本的通用解读在大型嵌入式应用软件开发中,如linux应用程序,这两个版本类型主要针对软件开发在不同阶段的构建过程的一些优化和设置,当然这些优化设置也是可以更改的。Debug版本主要是于开发和调试阶段,你所编写的软件通常被编译为未经优化的代码,以便于调试和排查。编译器会保留大量的符号信息、调试信息和原始代码结构,以便于在调试器中查看变量、函数调用栈等详细信息,有助于开发人员快速定位和解决问题。而对于release版本,经过了各种优化,以提高代码的执行效率和减小代码体积,比如去除未使用的代码、内联函数、循环展开、常量传播等,这样会使得程序具有较高的性能和较小的内存占用,适合于在实际运行环境中使用,但这也会大大削减一些调试信息和辅助检查。在大型OS上运行的应用程序所使用的两个版本均有如上特点,但对于单片机固件却有所差异,但也非常相似。2、单片机两种版本的特殊对于单片机软件系统中,这两个版本上的配置相对比较单纯点,主要是如下三个方面:1、优化级别:Debug版本通常使用较低的优化级别,例如-Og(优化以便调试),以便在调试过程中能够更好地观察变量的值和程序的执行流程。这种优化级别保留了较多的符号信息和原始代码结构,有利于开发人员进行实时调试和故障排除。Release版本则会使用更高的优化级别,例如-O2或者-O3(最大优化级别),以提升代码的运行效率和固件的性能。高优化级别会进行函数内联、循环展开、删除未使用的代码路径等操作,当然你也可以更细粒度的进行优化项目的设置,从而最大程度地优化代码的执行速度和资源利用率,但也会在一定程度上干扰开发软件对于程序与执行过程的对应分析。2、调试信息输出Debug版本通常会让程序中较多的debug_printf生效,这些打印调试信息不仅仅有第三方组件,还有一些自定义的调试信息,这样可以在一定程度上让开发人员更加直观的了解程序运行流程和结果,但也同时会导致程序执行负荷较高,体积变大。3、安全检查和错误处理:基本上也就是我们说的断言了,其实它就是在程序运行时检查程序中的某个条件是否满足预期,如果条件不满足,则程序会进入一个错误处理流程,通常是通过打印错误信息并可能进行一些清理工作后停止运行,如下代码示例:#includestdio.h#ifdef NDEBUG #defineASSERT(expr)((void)0)#else #define ASSERT(expr) ((expr) ? (void)0 : assert_fail( __FILE__, __LINE__ )) #endif void assert_fail(const char* file, int line) { printf("Assert failed at %s, line %d\n", file, line); while (1); // 可以添加清理代码,例如锁定资源,然后停止执行 } int main() { int x = -1; ASSERT(x > 0); // 这里的断言会失败,会调用assert_fail函数 return 0; } 所以当断言较多的时候,每个函数基本上都会对输入进行检查,执行效率可想而知。3、两个版本的必要性一般的公司基本上都只有一个debug版本,特别是单片机固件开发的项目更为突出。当然为了减少开发周期、又想保证产品功能的稳定性,处理器相对裕量较大,一直使用debug版本也不是不行,毕竟很多公司这些年就这么过来的,总比有些工程代码编译出来的release直接就飞了,还不知道怎么飞的好太多了,公司产品又急着上线,客户又要求快速解决,确实不容易;debug版本丢到现场,出了问题有日志,日复一日,终可修护稳定。不过从开发角度两个版本还是非常有必要的~release执行效率的提高、代码固件的压缩能够进一步降低核心主控的成本,同样对于执行效率敏感和内存资源受限的平台还是非常有用的,而且专业软件公司都会有单元测试与集成测试的开发与使用过程,release软件的稳定性能够得到较好的测试保障。同时debug版本通常比release版本更容易被反向工程,debug版本保留了大量的符号信息、调试信息以及原始代码结构,这些信息对于逆向工程者来说提供了更多的线索和可操作性。而且你打印了较多的调试信息,对于友商开发工程师而言也是很好的参考资料,相关功能也推敲个七七八八。当然话说回来,如果所开发的产品都是定制的话,并不需要去市场上争蛋糕,其实这块优势也会弱化。所以在软件稳定并且即将部署到目标设备时,尽量切换到Release版本,值得注意的是这里的release不是你测都没测,直接切换选项卡生成的版本,而是稳定发布版。正确选择和管理Debug版本和Release版本,能够有效地支持整个嵌入式软件开发生命周期的各个阶段。
最后一个bug
2 3 开源硬件平台
同步与异步,阻塞与非阻塞~
最近跟几个伙计交流技术的时候,经常性的把"同步与异步","阻塞与非阻塞"两组概念搞混,其实两者还是有着本质的区别的,更何况还有"同步阻塞"、“异步非阻塞”等等各种组合,那么今天大致跟大家一起聊聊:1、同步与异步同步和异步区别在于当你的程序处理一项事务时是否需要等待 操作完成才能继续执行其他任务。同步表示的是顺序执行,当程序执行一个任务需要的等待任务完成,才能继续往下执行。而对于异步则是发起一个任务后不用等待任务执行完成,就可以继续往后执行,而异步的任务处理通常通过回调函数、事件监听来处理任务完成或数据准备就绪。所以总结起来,同步和异步描述的是被调用方相对调用方的处理方式。同步:按照顺序执行任务,任务之间有依赖关系,一个任务完成后才能进行下一个任务。异步:不按照顺序执行任务,任务之间可以并列执行,一个任务开始后可以立即执行下一个任务,不必等待处理结果。在C语言程序里面同步和异步的差别,你就可以认为是任务函数所调用的时机。单片机中断处理是一种典型的异步事件处理机制。当一个中断事件发生时,处理器会立即跳转到中断服务程序(ISR),这种情况可以看作是异步的,中断的发生与主程序的执行流程是分离的。2、阻塞与非阻塞这里看到阻塞可能大家疑惑了,同步不就是阻塞当前线程等待返回继续往后执行吗?这句话不对,同步也可以不阻塞当前线程不断地轮询等待正确的返回后继续往后执行。所以阻塞与非阻塞关键看发起任务执行后当前线程是否让出CPU而挂起,阻塞与非阻塞描述的是等待消息通知时的线程的状态。异步处理强调的是非阻塞和并发执行,适用于需要高效利用系统资源、快速响应的场景;而同步处理则更侧重于任务的顺序执行和阻塞等待,适用于依赖严格顺序和同步操作的场景。
最后一个bug
1 2 立创开发板
物联网的万车互联很有必要
1、目前的辅助感知算法水平还有很大的进步空间,通过摄像头、雷达等识别方式限制性实在是较大,在高速上偶尔来个刹车也是非常要命的。 所以结合无线通信、卫星等组合式的辅助驾驶,实现汽车之间的互联与信息共享变得非常有意义了,一旦前车出现重大异常便可以最早的提醒后车进行避让,大幅度降低事故损失。 2、人机交互目前还停留在XX同学的智能语音交互系统,听听歌、导导航等等,这多多少少还是低端了点。 #畅聊专区# 醉驾、病驾、疲劳驾驶等识别基本都没有部署。
最后一个bug
1 8 开源硬件平台
嵌入式实时性可以考虑静态链表~
大家好,我是bug菌~首先跟大家聊聊什么是静态链表,静态链表是一种使用数组来实现的链表结构。在静态链表中,数组的每个元素称为一个节点,节点中包含两部分信息:数据和指向下一个节点的“指针”,这里的指针并不是C语言里语法上的指针,它主要是标记节点在数组中的位置,也就是数组的下标索引,其实广义上也是一种指针吧。再来看下静态链表怎么玩的吧~所以与动态链表的差异点,主要是静态链表的节点在内存中是连续存储的,而且节点的数量是固定的。有代码有真相:#include#define MAX_SIZE 100 // 静态链表的节点结构 typedef struct Node { int data; // 数据域 int next; // 指针域,指向下一个节点的索引 } Node; // 初始化静态链表 void init(Node list[]) { // 将所有节点的 next 域初始化为 -1,表示空闲状态 for (int i = 0; i < MAX_SIZE; i++) { list[i].next = -1; } } // 释放节点 void release(Node list[], int index) { // 将节点标记为可用状态 list[index].next = -1; } // 获取可用的空闲节点索引 int getFreeNode(Node list[]) { for (int i = 0; i [removed]
最后一个bug
3 9 开源硬件平台
Keil中三种手动结构体对齐方式,别用错了~
最近移植了一些开源组件,发现较多的语法跟编译器相关,如果没有跨平台处理,确实大大降低了程序的可移植性,其中尤为突出的就是结构体字节对齐属性的标识,通常编译器采用默认字节对齐方式,按照处理器架构的要求来决定的。比如如下结构体在stm32中默认为4字节对齐: typedef struct _tag_Test1 { uint8_t member1; uint16_t member2; }sTest1; stSize = sizeof(sTest1); 自然sizeof获得的结构体大小也是4。然而默认对齐方式有时候并不满足我们编程的需求,比如需要降低一些内存占用,或者提高相关数据的访问效率等等,我们会手动的声明相关变量的对齐方式。那么这里总结了下AC5编译器进行字节对齐的几种方式:1、#pragmapack#pragma pack 是一个编译指令,用于指定结构体、联合体和类成员的字节对齐方式。在 Keil uVision5 中,可以使用 #pragma pack 指令来设置字节对齐方式。一般我们用如下方式标识#pragma pack(n)其中,n 是对齐系数,表示按照 n 字节对齐。常见的对齐系数包括 1、2、4、8 等。例如,若要将对齐系数设置为 4,比如:#pragma pack(4),该指令通常放置在结构体、联合体或类的定义之前,以影响其后的所有定义,这里尤其需要注意,很多时候忘记恢复字节对齐导致了一些没必要的问题。这样一来,所有在 #pragma pack 后声明的结构体、联合体或类成员都将按照指定的字节对齐方式进行排列。那么如果我们需要取消则需要采用#pragmapack()来取消结构体对齐。#pragma pack (1) typedef struct _tag_Test2 { uint8_t member1; uint16_t member2; }sTest2; #pragma pack () // 取消结构体对齐 stSize = sizeof(sTest2); 通过这样的定义,使得sTest2结构体整体大小只占用3个字节在,这种方式在MDK中比较常用。当然有经验的朋友该说了,我用#pragma pack(push,n)比较多,没错,该语法也同样是可以的,比如例子: //#pragmapack(1)#pragma pack(push,1) typedef struct _tag_Test2 { uint8_t member1; uint16_t member2; }sTest2; //#pragmapack()//取消结构体对齐#pragma pack(pop) stSize = sizeof(sTest2); 既然都聊到这个份上了,该谈谈他们的差异了:#pragmapack(n)和#pragma pack(push, n) 其实在功能上没太大的区别,仅仅只是在使用方面略有不同。#pragma pack(n):这个指令直接设置当前字节对齐系数为 n。这意味着在此指令之后声明的结构体、联合体或类成员都将按照指定的字节对齐方式进行排列。每次使用 #pragma pack(n) 时,都会覆盖之前的对齐设置,因此它可能会影响后续的代码。没有保存当前的对齐方式,因此在使用完之后,如果需要还原到先前的对齐方式,就需要手动重新设置。#pragma pack(push, n):这个指令其实也是设置当前字节对齐系数为 n。但是它还有一个功能,就是将当前的对齐方式保存到编译器的栈中。这意味着,使用 #pragma pack(push, n) 后,可以在代码的后续部分使用 #pragma pack(pop) 来恢复之前保存的对齐方式,而不会受到之后代码中 #pragmapack指令的影响。因此,#pragma pack(push, n) 更灵活,可以避免在代码的后续部分不小心修改了对齐方式而导致错误。2、__attribute__((__packed__))__attribute__((__packed__)) 是 GCC 和一些兼容 GCC 的编译器(如 Clang)提供的一个特性,用于指示编译器以紧凑的方式存储结构体或类,即取消对齐。使用案例如下: typedef struct __attribute__ ((__packed__)) _tag_Test3 { uint8_t member1; uint16_t member2; }sTest3; stSize = sizeof(sTest3); 那么最终结构体的大小也将是3个字节。3、干脆的__packed相信有经验的各位都比较喜欢使用这个属性吧: typedef __packed struct _tag_Test4 { uint8_t member1; uint16_t member2; }sTest4; ; stSize = sizeof(sTest4); __packed 是一种特性,指示编译器取消对其成员的自然对齐。它的作用类似于 __attribute__((__packed__)),但是它更加与平台无关,因为它是一种更通用的约定,不依赖于特定编译器。虽然说__packed 是一种常见的约定,但它并非标准 C 语言的一部分,因此在不同的编译器和平台上可能具有不同的行为,使用时应谨慎考虑平台兼容性和性能问题。
最后一个bug
1 7 立创开发板
嵌入式直接操作内存即是C语言的优势也是劣势~
最近被老项目的一些问题折腾得不轻,同事也都吐槽要不是大客户,他们连代码都不想同步到本地。这个项目从bug菌来的时候就已经开发了好几年了,早期团队较小,估计软件上也没怎么管理,人员也是换了一批又一批,工程之大,功能之多,现在拿出来基本都是公认的”屎山”了,那块业务也没咋做了,都不想再花人力再去重构,那就只能大家有时间的修修bug加一些简单的需求了,不过C语言的有些bug可真不好找。大家都知道C语言是非常高效的一门编程语言,像linux内核都是C语言来开发的,在嵌入式开发中C语言目前还是主流,虽然也受到了一些其他新型编程语言的冲击,但很多取舍都是一种折中的方案,毕竟熊掌和鱼不可兼得。C语言是一种内存不安全的编程语言,然而C语言的优势却又于程序员可以直接操作内存带来的高效,这就让很多人在原则上处于一种矛盾状态,其实完全没有必要纠结,没有完美的编程语言,只有是否满足你现在的需求。当然由于C或者C++的广泛应用,有调查显示,70%比例的漏洞和bug都来源于内存安全问题。这也让很多人在开发语言的选择上进行了深入地考量。说白了还是因为C语言在对内存管理和访问方面的自由度和灵活度太高了,操作的权限范围太大。一份C语言代码的高效、稳定、可靠、健壮很大程度上都取决于所编写代码工程师的水平。当然除了程序员通过自身经验在编码中去识别,也有非常多的软件分析工具会自动检出大部分内存管理问题,并且操作系统也会在中间层提供部分保护,但相对那些内存安全的编程语言比,如Python,Java, C#, Go,以及扬言要取代C的Rust,他们可以自动管理内存,不依赖于程序员添加措施进行保护,编译和运行时都会进行检查和保护。当然编程语言通过固有的保护和缓解措施建立的安全程度各不相同。有些语言只提供相对最低的内存安全性,而有些语言非常严格,通过控制内存的分配、访问和管理方式提供了较大的保护,所以相比之下C在内存安全等级较低。内存泄漏,数据篡改、内存缓存区溢出、随机的程序奔溃、程序执行指令被异常修改等等都是C语言开发工程师们经常要面对的头皮发麻的问题。而且通过利用这些类型的内存问题,恶意攻击者可能会发现他们向程序中输入异常,导致内存以意想不到的方式被访问、写入、分配或释放。利用这些内存管理错误来访问敏感信息、执行未经授权的代码或造成其他负面影响。当然由于可能需要对异常输入进行大量实验才能找到导致意外响应的输入,参与者会使用一种模糊测试技术,随机或智能地为程序制作大量输入值,直到找到导致程序崩溃的输入值,最终拿到想要的破译结果。所以在一个非常庞大的C语言代码,如果早期就比较混乱,后面维护人员的技术功底不扎实,必定会使得后面莫名其妙的问题越多;最终只能走上一条重构之路。
最后一个bug
0 3 立创开发板
PID 控制算法真的强~
这种 “现在、过去、未来” 的类比,本质是通过时间维度的直观理解,帮助记忆 PID 三项的不同作用: P(现在):快速响应当前误差; I(过去):消除历史累积的稳态误差; D(未来):抑制未来可能的剧烈变化(超调)。
最后一个bug
3 6 立创开发板
提升嵌入式软件设计认知瓶颈
大家应该都经常听到“什么要提升认知”、“人只能赚到个人认知以内的money”等等。认知其实就是当你获取、处理和理解信息的心理过程,包括感知、记忆、思考和决策等,其实做技术一样的也有对技术认知。当面对一个设计、一个bug,会从哪些维度去思考,准备怎样去处理,一些工程师立马就会进去无尽的debug and step,而有些工程师却能不慌不忙的用一些“偏门的”调试技巧和方法就能分析个大概,然后稍加调试就能直击问题的要害,又或者构建的系统中原本就有这种方法,他知道而你却不知道。那在嵌入式软件开发过程中一般会面临哪些技术方面的认知瓶颈呢?1、软件系统复杂性管理系统功能越来越多,软件越来越大,如何有效管理和组织复杂的软件架构成为开发中的一大挑战,如今很多工程师常常只是负责自己那一小部分功能,核心算法功能实现估计都看不到,难以掌控系统的整体结构。2、实时性工业嵌入式系统通常需要满足严格的实时性要求,理解和深入不同实时操作系统的不同模式下的调度策略,以及给自己的任务进行最优的优先级管理是一个难点。3、资源受限嵌入式设备通常只有有限的计算能力和内存,需要在效率和资源使用之间找到平衡,压榨硬件,节省成本,这对设计和优化提出了较高的要求。4、硬件平台抽象化(嵌入式的跨平台)这里并不谈linux与windows所谓的应用程序的跨平台,而是不同硬件平台和架构之间的抽象与统一,否则会导致代码重用性低和维护成本高。5、安全性随着物联网的发展,嵌入式系统面临着越来越多的安全威胁,如何设计安全可靠的系统仍然是一个重大挑战,还有固件会不会被恶意提取,反编译等问题都是一个成功产品需要考量的。6、运行稳定与可靠性24h、72h稳定运行工况,掉电不损坏系统,各异常能够正常捕捉与上报,都成为优秀系统设计中的一大考量点。7、软件更新与维护嵌入式设备如果在生产后难以进行远程更新,那肯定是一大设计败笔,如何设计便于后期维护和更新的系统架构是一个挑战。8、调试调试工具的熟练度和丰富度,不同的问题和现象采用不一样的调试手段和技巧。9、测试自动化尽管测试对确保软件质量至关重要,但许多嵌入式系统尚未完全实现测试自动化,增加了人工测试的负担。
最后一个bug
2 18 硬创社
详解程序中的“段”是咋回事~
1、段的概念在计算机架构中,段的概念是用于组织和管理内存的一种方式,特别是在早期的 x86 架构中,什么8086处理器啥的,不过现在很多概念一直沿用。每个段代表了内存中的一个逻辑区域,用于不同类型的数据或代码。这种分段机制可以提高程序的灵活性和安全性。下面是一些常见的段类型:1. 代码段 (Code Segment, CS)代码段主要是存储程序的可执行指令。该段只读,防止程序在运行时意外修改其自身的指令。包含所有执行的逻辑,通常以机器指令的形式存在。2. 数据段 (Data Segment, DS)数据段存储程序运行所需的全局变量和静态变量。该段可读可写,允许程序在运行过程中修改数据。初始化部分存储初始值,未初始化部分(BSS 段)通常会被默认设置为零。3. 堆栈段 (Stack Segment, SS)定义:堆栈段用于存储函数调用时的局部变量、返回地址以及其他临时数据。该段采用后进先出(LIFO)的结构。支持函数调用和返回,存储参数和局部变量。当然我们分析elf文件的时候会发现还有很多类型的段:.text:包含可执行代码。.data:包含已初始化的全局变量和静态变量。.bss:包含未初始化的全局变量和静态变量,其大小在程序加载时会被设置为 0。.rodata:包含只读数据,例如字符串常量。.symtab:符号表,存储程序中的符号信息。.strtab:字符串表,存储符号名称和其他字符串。.rel 或 .rela:重定位信息,用于链接器处理地址修正。2、段带来的好处内存保护:通过不同的段,可以对内存区域进行访问控制,防止程序错误地修改其他段的内容。模块化:代码段和数据段的分离使得程序的结构更加清晰,有助于模块化设计和维护。动态内存管理:分段允许程序在运行时动态地请求和释放内存,提高了内存利用率。虽然在现代计算机架构中,分段机制可能不再广泛使用,但其基本思想仍然影响着内存管理的设计和实现。3、段的特点段内的地址通常被视为连续的。这意味着在一个特定的段(如代码段、数据段或堆栈段)内,地址是顺序排列的,从段的起始地址到段的结束地址。连续性:段内的地址是连续的,这使得访问和管理内存更为简单。例如,在数据段中,变量的地址会依次排列,便于程序在运行时进行访问。逻辑划分:虽然物理内存可能是不连续的,但逻辑上每个段的地址范围是连续的。操作系统通过内存管理单元(MMU)来处理虚拟地址与物理地址之间的映射。段的大小:每个段的大小可以不同,代码段可能比数据段大,或者相反。但在每个段内部,地址是线性且连续的。分隔符:段之间的地址通常不连续,段的开始和结束由段寄存器管理。例如,如果一个段的起始地址是 0x1000,而下一个段的起始地址是 0x2000,那么这两个段之间的地址并不连续。4、段寻址示例8086的分段寻址算是最早期、最简单的分段机制了,该分段寻址过程涉及段寄存器和偏移量的组合,即段寄存器内存左移+偏移量便得到了索要访问的地址。那为什么需要将段寄存器的内容左移 4 位并与偏移量相加,很多朋友并不是很理解,我们可以从以下几个方面进行解析:在 x86 架构中,段寄存器包含的是段的基地址,而这个基地址是以 16 字节为单位的。偏移量的概念:偏移量是指在指定段内的具体位置,它是以字节为单位的。物理地址的计算:段寄存器的值左移 4 位(即乘以 16)是因为其内容实际上表示的是段的起始地址,以 16 为单位。例如,如果段寄存器中的值是 0x000A(十六进制),那么左移后得到的物理地址为 0x000A0。将左移后的段基地址与偏移量相加,就可以得到实际的物理地址。1MB 的内存限制:在 x86 体系结构中,20 位的物理地址可以表示最大 2^20 = 1MB 的内存空间。这是因为 20 位二进制数的范围是从 0 到 1,048,575(0x000000 到 0xFFFFF)。所以将段寄存器的内容左移 4 位后加上偏移量,我们能够综合得到一个 20 位的物理地址,从而能够有效访问最多 1MB 的内存空间。虽然现代计算机系统中没有在选用这种方式,但核心思想并没有发生很大的变化。
最后一个bug
4 13 立创开发板
芯片的唯一标识符有什么作用?
今天主要是大家聊聊设备唯一标识符:1、聊聊唯一标识符早期大部分芯片都没有唯一设备标识符UID,英文叫Unique ID,现在去查查其实很多芯片现在也没有唯一标识,然而随着芯片成本降低,功能上大家基本都对齐了,新推出的芯片都会有一个唯一标识码,通常这个编码在芯片制造的过程中就生成了,用户通常读取固定地址或者调用相关API即可轻松获取。当然了这些UID通常不是随机的,都有一定的规律,比如标识制造商、批次、芯片型号等等,所以通常厂商会根据UID的部分字段做一些功能的区分。有一点一定要注意:对于相同型号的芯片,其UID通常是唯一的,但不同型号的芯片,尽管是同一家公司,其UID也有可能不是唯一的。因为之前遇到过这个问题,所以特意提示下,这一点一些人容易有惯性思维。2、UID有那些应用?那么唯一标识符到底有啥用处呢?字面上那肯定是为硬件设备提供唯一性,以便区分罢了,但具体涉及到哪些方面会要用到唯一标识区分呢?下面我总结了几个方面:1、产品唯一标识许多不同的设备可能会共享同一种硬件平台,每个设备都分配一个唯一标识符,系统可以确保每个设备在全球范围内都是唯一的,避免了设备间的冲突。像现在许多的IoT设备,其中的每个传感器或控制器可以通过唯一标识符进行识别和管理,这对于设备的监控、配置以及后续维护是非常重要的。2. 设备身份认证在一些需要身份验证的应用场景中,唯一标识符可以作为设备认证的一部分。在系统初始化或进行安全通信时,通过标识符验证设备的合法性,从而提高安全性。现在有很多的智能家居的产品,所有设备(如智能门锁、摄像头等)在联网时可以使用唯一标识符来进行身份认证,这些标识符提前录入了系统,确保只有经过授权的设备能够接入系统。3. 版本控制和固件更新通过设备的唯一标识符,厂商可以为特定设备提供定制化的固件更新或配置管理。这样一来,即使是相同型号的多个设备,也可以根据其唯一标识符来执行不同的操作或更新。4. 系统完整性绑定多个设备的唯一标识符可以进行捆绑,当检测到标识符不匹配可以进行报警提示,防止系统被拆解,从而带来的一系列混乱、不匹配问题。5. 软件许可和防盗唯一标识符可以作为本地软件运行的一种许可管理和防盗机制。设备唯一标识符可以与授权许可绑定,也就是相当于一种密钥,防止不法商家盗版软件的使用或设备被非法复制。通过唯一标识符与授权信息绑定,厂商可以确保只有授权的设备才能使用特定功能,防止盗版设备影响正常运行。
最后一个bug
1 6 硬创社
MCU的App直接卡死,IAP升级方案如何解决?
最近被客户搞得一愣一愣的,你跟他谈技术,他跟你无话可谈,一直待在自己的认知里,当然客户是上帝,还得求着跟他谈,态度还得好,属实无奈~相信搞技术的朋友最初有很大部分还是因为技术的纯粹与客观,然而随着职业的发展,不断地往上攀升就会发现似乎蒙头搞技术很难"吃遍天",大一点的项目就得跟客户搞关系,并且得客户认可你的技术价值,难以独善其身,也就会变得越来越不纯粹了。好了,还是聊聊今天的技术内容。1、问题背景大家在单片机程序开发升级功能的过程一定遇到过这种尴尬的场景:基础的IAP升级功能一切正常,然而由于你修改了App,在App中不小心引入了一个致命bug,编译成功直接生成了烧录文件,且生成了对应的升级文件完整性校验,当你把该完整的升级文件烧录完成以后,哦豁,完蛋,boot校验App文件校验通过,运行App直接卡死,你尝试重新上电同样却还是继续跳转卡死。此时的你只能乖乖的找来烧录器如jlink进行调试找出这个卡死App的bug并重新烧录,当然这在开发的过程中还相对比较简单,但如果电路板没有引出烧录接口且被封装在了一个非常难拆的机器内,那这个过程工作量就大了。是否有其他设计方案能够规避掉这种问题呢?2、常规处理方式对于boot自带通信功能(如串口、USB等)的系统,可以在boot跳转App前检查是否存在更新App请求,如果存在则进入升级模式,把原有异常的App替换掉。这样的方案其实在uboot或者电脑的BIOS中都能看到影子。3、变体上面的方案根据项目外部的交互接口不同,可以进一步演变,上面我们介绍的是等待通信命令,然而有些项目并没有通信接口等,只有按键,那么可以在boot跳转App前检测是否对应的按键长时间强制按下,而进入升级请求状态,这种方式在一些消费类的小项目中比较常见。4、不想重启大家应该有注意到,上面的方案麻烦点的就是要重启,有复位按键再好不过了,然而有些系统掉电时间较长,或者不允许外部异常复位,那重启操作起来就比较费时。最常用就是应用程序自身开启了watchdog,这样当我们不小心烧录了卡死程序,没有及时喂狗就会导致重启进入boot,然后又可以进入前面介绍的强制升级流程了。当然对于对开机速度有要求的项目,可以在boot里检测是否为watchdog复位再让延时等待请求介入。5、boot比较小有时候我们不想把boot设计得非常复杂,驱动太多需要改来改去,那么可以采用AB面升级方式。如果当前升级的App异常,也可以重新上电回退到上一个可用App版本,这样在上一个版本进行新App的更新。
最后一个bug
0 0 开源硬件平台
RS485的DE与RTS有什么区别?
#DIY设计# 设计初衷:DE专为RS-485驱动器控制设计;RTS原是RS-232的流控制信号。 控制方式:DE直接由应用层控制,RTS可能通过UART硬件或协议层间接控制。 时序要求:DE需严格满足驱动器使能时序,RTS的时序可能与UART数据帧同步。
最后一个bug
0 0 硬创社
嵌入式软件设计中的依赖反转原则
今天跟大家聊聊软件设计中依赖反转这一基本原则。在软件设计中,依赖反转原则(英文:Dependency Inversion Principle, DIP)是面向对象设计原则的一部分,旨在提高系统的灵活性和可维护性。虽然C语言本身不是直接支持面向对象无法特性的语言,但我们仍然可以在C语言中实现类似的概念和选择思想。一、依赖反转原则基本思想这个原则的主要思想就两点:高层模块不应依赖于低层模块,两者都应依赖于抽象(接口)。抽象不应依赖于细节,细节应依赖于抽象。这句话怎么理解呢?以图形绘制系统为例,绘图的抽象类或者接口不依赖于具体的图形(如圆形、矩形这些细节)如何绘制。相反,具体图形的绘制类要实现绘图接口规定的方法,即细节依赖抽象。这使得系统更灵活,方便添加新图形种类。在C语言中的实现虽然C语言没有类和接口的概念,但可以通过函数指针来实现依赖反转。以下是一个简单的例子:#include // 定义一个函数指针类型,用于表示抽象行为 typedef void (*LogFunction)(const char*); // 低层模块:具体实现 void ConsoleLogger(const char* message) { printf("Console: %s\n", message); } void FileLogger(const char* message) { // 假设将消息写入文件的代码 printf("File: %s\n", message); } // 高层模块 void Application(LogFunction logger) { logger("Hello, Dependency Inversion!"); } int main() { // 使用控制台日志记录 Application(ConsoleLogger); // 使用文件日志记录 Application(FileLogger); return 0; } 代码简单分析如下:LogFunction:定义了一个函数指针类型,用于实现不同的日志记录策略。ConsoleLogger 和 FileLogger:这两个函数是低层模块,负责具体的日志记录实现。Application:这是高层模块,它依赖于LogFunction类型的抽象,而不是具体的日志实现。二、相对复杂一点的例子 以下是一个简单的C语言示例来体现依赖反转原则:#include // 抽象接口:定义了形状的绘制操作 typedef struct Shape Shape; struct Shape { void (*draw)(Shape*); }; // 具体形状:圆形 typedef struct Circle { Shape base; double radius; } Circle; // 圆形的绘制实现 void circle_draw(Shape* shape) { Circle* circle = (Circle*)shape; printf("Drawing a circle with radius %.2f\n", circle->radius); } // 具体形状:矩形 typedef struct Rectangle { Shape base; double width; double height; } Rectangle; // 矩形的绘制实现 void rectangle_draw(Shape* shape) { Rectangle* rectangle = (Rectangle*)shape; printf("Drawing a rectangle with width %.2f and height %.2f\n", rectangle->width, rectangle->height); } // 主函数,用于测试 int main() { // 创建圆形对象并初始化 Circle circle; circle.base.draw = circle_draw; circle.radius = 5.0; // 创建矩形对象并初始化 Rectangle rectangle; rectangle.base.draw = rectangle_draw; rectangle.width = 4.0; rectangle.height = 6.0; // 通过抽象接口调用绘制操作 Shape* shapes[2]; shapes[0] = (Shape*)&circle; shapes[1] = (Shape*)&rectangle; for (int i = 0; i [removed]draw(shapes[i]); } return 0; } 首先定义了一个抽象的 Shape 结构体,它包含一个函数指针 draw ,代表了绘制形状的抽象操作,这相当于依赖反转原则中的抽象部分。然后分别定义了具体的形状 Circle 和 Rectangle ,它们都包含了 Shape 结构体作为其一部分,并各自实现了对应的绘制函数( circle_draw 和 rectangle_draw ),这是细节依赖抽象的体现。在 main 函数中,可以通过指向 Shape 结构体的指针数组来统一处理不同的具体形状对象,而不需要关心它们具体是哪种形状,只需要调用抽象接口中定义的 draw 操作即可。这样高层模块(这里的 main 函数可以看作相对高层的模块,负责协调不同形状的绘制)不依赖于具体的形状模块(圆形或矩形的具体实现),而是依赖于抽象的 Shape 接口,符合依赖反转原则。如果后续要添加新的形状,只需按照类似的方式定义新形状结构体并实现对应的绘制函数,使其符合 Shape 抽象接口,就可以很方便地集成到系统中。三、总 结通过使用函数指针,我们可以在C语言中实现依赖反转原则。这种方法使得高层模块与低层模块解耦,提高了系统的灵活性和可测试性。灵活性当系统的某个具体模块(细节)需要修改时,只要它所实现的抽象接口不变,依赖于这个抽象接口的其他模块几乎不需要修改。例如,在一个物流系统中,运输方式(如陆运、水运)的具体实现细节改变,只要运输方式的抽象接口(如计算运费、预计到达时间)不变,依赖该接口的订单管理等高层模块不用修改,维护起来更方便。可测试性增强可以方便地使用模拟对象(Mock)来替代真实的依赖对象进行单元测试。比如在开发一个支付系统时,对于支付网关这个依赖项,在测试时可以通过模拟一个符合抽象接口规范的假支付网关,来测试支付处理模块的功能,而不用依赖真实的支付网关,使得测试更容易进行。软件的可扩展性变好便于添加新功能。在遵循依赖反转原则的软件架构中,添加新的功能模块(细节)只需要新模块实现现有的抽象接口就行。例如,在一个图形编辑软件中,要添加新的图形绘制功能,只要新的图形绘制类实现绘图的抽象接口,就可以方便地集成到系统中,而不会对其他模块产生较大影响。
最后一个bug
0 4 立创开发板
聊聊时间敏感性TSN网络
最近大家应该有看到一些强劲的开发板介绍中有提到TSN这样的字眼,那么今天就来跟大家聊聊特色功能:1、什么是TSN ?TSN英文为Time - Sensitive Networking即时间敏感网络,是一组 IEEE 802.1 标准定义的以太网网络协议。数据传输的时间确定和低延时是它的两个主要特点,目前在一些工业控制领域、或者是对实时性和可靠性要求极高的场合中用起来了。对于TSN主要是上了这三种技术手段来保障其实时和低延时的特点。1、首先肯定是要有精确的时间,像rt-linux等等都需要采用高精度的定时器,同样对于TSN采用的是像IEEE 1588 精密时间协议(PTP)来同步网络中各个节点的时钟。就好比在一个工厂自动化系统中,不同的机器人手臂、传感器和控制器他们通过网络来进行交互与控制,我们老的控制系统像平衡车这种被控对象、采集模块、动力驱动等基本都由一个芯片控制进行实时反馈控制,而现在他们通过以太网进行连接控制,对于采集信号什么时候能够到控制器就有了严格的要求,那么通信交互的系统中需要有同步且精确的时间,实时网络系统中通过 PTP,这些设备可以将时钟同步到亚微秒级精度,保障了数据能够按照预定的时间顺序达到。2、流量的调度的问题,那就是一个优先级的问题,有些只是用来显示的信号,我们分配到特定的时间槽,不能堵塞高优先级的控制数据。3、TSN不仅仅不保障数据要低延时达到,也要保障数据正确的交付,这时候就有一些冗余机制,如 IEEE 802.1CB 的帧复制和消除用于可靠性(FRER),一条传输路径出了问题,还有其他冗余路径,保障可靠传输。2、lwip不支持TSN没错lwip目前算是在MCU上用得非常广泛的开源协议栈,不过它不直接支持TSN,毕竟lwip的设计功能定位有关系,它主要是提供一个轻量级的 TCP/IP 协议栈,重点还是在于实现基本的网络通信功能,在一些资源受限设备上跑跑。像大家在移植lwip的时候应该没有进行高精度协议的处理吧,毕竟TSN也是后来才形成的标准,而lwip 的协议架构还是基于传统的 TCP/IP 模型,与 TSN 所涉及的一系列标准和协议在架构和功能上存在较大差异。TSN最早应该是为了主要解决以太网中音频视频数据实时同步传输的问题,确保其在正确的时间到达以实现同步,同时还需保证交付吞吐量和延迟,避免出现丢失视频帧等问题,后来才有了TSN 工作组并制定了一系列 IEEE 802.1 标准,这些标准从不同方面定义了 TSN 的功能和机制,包括时间同步、调度和流量整形、通信路径的选择预留和容错等关键组件,后来工业自动化和汽车的发展对通信带宽和实时性延时等等提出要求才进一步细化完善。3、与任务优先级问题在传统的以太网编程中,为了让以太网协议栈处理的及时性,通常会把协议栈任务优先级设置较高,以及时响应并保证网络通信的稳定性和正确性。因为网络协议栈的底层操作往往需要快速地处理接收到的数据包、进行链路维护等操作。如果这些操作不能及时进行,可能会导致数据包丢失、网络连接中断等问题。例如,当一个新的 TCP 连接请求到来或者有数据需要立即发送时,协议栈处理线程能够快速地启动协议栈相关的处理流程。然而到了TSN网络编程中,因为TSN要求较高的吞吐量时,应用程序任务优先级低于网络协议栈可能会产生问题。因为 TSN 应用通常有严格的时间要求和数据传输要求例如,在一个工业自动化的 TSN 场景中,应用程序任务可能负责控制电机的精确运动或者传感器数据的实时采集和处理。如果应用程序任务的优先级低于 PrvIPTask,那么在网络负载较重或者 PrvIPTask 频繁执行的情况下,应用程序任务可能会被长时间阻塞。假设应用程序任务需要每 10ms 发送一次控制指令来精确控制电机的速度,但是由于 PrvIPTask 一直在占用 CPU 资源处理网络协议栈相关的事务(如频繁地接收和处理一些非关键的网络管理数据包),应用程序任务无法按时执行,这就会导致电机速度控制不准确,影响整个 TSN 系统的性能和功能实现。所以在这种 TSN 要求高吞吐量的场景下,需要重新评估任务优先级,可能需要提高应用程序任务的优先级,以确保关键的应用功能能够及时执行。
最后一个bug
0 1 立创开发板
C++ ​​适合​​单片机开发吗?
如果采用C++ 进行单片机开发,但需遵循以下原则: 选择性使用特性:优先使用类、模板、RAII,禁用异常/RTTI。 硬件级优化:结合寄存器操作和内联汇编。 资源控制:避免动态内存分配,控制代码体积。 对于中高端 32 位单片机(如 Cortex-M3/M4),C++ 能够显著提升代码组织性和可维护性,同时保持接近 C 的性能。但对于入门级单片机,C 仍是更稳妥的选择。
最后一个bug
0 0 立创开发板
单片机运行实时性与快速执行的差别
#畅聊专区# 实时性和运行速度快有所不同。实时性强调系统对外部事件在严格时间期限内做出响应和处理,重点在于满足特定的时间要求,确保任务在规定时间内完成,以维持系统功能的正常与稳定,如工业自动化控制。运行速度快则侧重于系统执行任务的效率高,能在较短时间内完成大量工作,但不一定有严格的时间限制,比如高性能计算系统,主要追求计算速度快,对结果的输出时间没有像实时系统那样苛刻的要求。
最后一个bug
0 2 立创开发板
开源轻量级printf,修一修就能跑~
有一些朋友问到有没有开源的printf直接可以拿来用的,不想再重复造轮子了,一些老维护项目软件架构也不能随便换,只是想加入这个组件方便以后排查问题,那当然是有的,毕竟开源界的道友们还是非常无私的。那么接下来就给大家推荐三个轮子,后续自己根据实际项目需求进行修一修基本就能用了~1、xprintfxprintf 是一个紧凑的字符串 I/O 库。 它非常适用于程序存储器不足以用于常规 printf 函数的微型微控制器。 推荐的用途是:将格式化的字符串写入 LCD 或 UART 以及用于调试/维护控制台。可以使用配置选项配置 xprintf 以减小模块大小。下表显示了 Cortex-M3 (gcc -Os) 中代码大小的示例。 long long 和 float 需要 C99 或更高版本。源码和使用说明都在如下路径:http://elm-chan.org/fsw/strf/xprintf_j.html2、lwprintflwprintf-Lightweight printf library optimized for embedded systems,lwprintf是针对嵌入式系统优化的轻量级 stdio 管理器。 用 C 语言 (C11) 编写,实现了与 printf、vprintf、snprintf、sprintf 和 vsnprintf 兼容的输出函数,只需要几 kB 的非易失性存储器,较低的内存占用,适用于嵌入式系统.并且对所有 API 函数的可重入访问,能够在多个线程打印到同一输出流可选支持,还允许多个输出流函数(与仅支持一个输出流函数的标准不同)来分离应用程序的各个部分。所以整体lwprinf功能的选择会更加的丰富,而且这个项目文档案例也比较丰富,根据自己的情况进行功能的选择,挺香的。开源地址如下:https://github.com/MaJerle/lwprintf
最后一个bug
0 5 开源硬件平台
UDP通信哪有那么不靠谱呀~
大家好,我是bug菌,又见面了~最近在做设计评审时,下面工程师在对用UDP和TCP中展开了激烈的讨论,而一个同事发表了这样的观点:"能用TCP绝不用UDP,UDP实在是太不靠谱了~"说实在的我也很理解,但是对于这样的言论也容易误导人,导致很多初学者在尝试使用UDP设计的时候,只要出现一些埋得比较深的问题就会一棍子把UDP给拍死,这是我觉得不应该的。其实UDP并没有那么不靠谱。1、UDP数据包首先我们看看UDP的数据包格式,数据内容本身的正确性是由UDP的**校验和(Checksum)**机制保证的:如果数据包在传输中发生比特错误(如电磁干扰),接收端会直接丢弃该包,不会将错误数据传递给应用层。然而UDP的“不可靠”主要是如下三点:不保证数据包到达(可能丢包)不保证顺序性(可能乱序)不主动控制发送速率(可能拥塞丢包)所以有些数据错乱问题的过,真不该让UDP来背,往往有时候就是自己设计的应用层逻辑设计不当。比如下面常见的一些场景:场景1:乱序未处理• 问题:接收端先收到后发送的包(如视频流的第2帧早于第1帧到达)。• 根因:应用层未实现乱序重组逻辑,直接按接收顺序处理数据。• 改进:在数据包头部添加序列号(Sequence Number),由应用层缓存并排序。场景2:分包/合包逻辑缺失• 问题:发送端传输了超过MTU的大数据包,IP层自动分片,但接收端未正确处理。• 根因:应用层未实现手动分包/合包逻辑,依赖IP分片(可靠性差)。• 改进:在应用层主动将大数据拆分为≤1472字节的块,并为每个块添加序号和偏移量。场景3:未区分丢包与延迟• 问题:误将延迟较高的包视为丢失,导致逻辑错误。• 根因:未设置合理的超时重传机制。• 改进:为关键数据添加ACK确认与重传,非关键数据允许丢弃(如实时音视频)。2、丢包原因其实UDP的丢包主要还是与网络环境有关,网络拥塞:路由器缓冲区溢出时,UDP包被优先丢弃(TCP会主动降速,UDP不会)。链路质量差:无线网络(如Wi-Fi、4G)易受干扰,物理层丢包率上升。IP分片丢失:若数据包超过MTU被分片,任一碎片丢失会导致整个UDP包不可用。虽然会丢包,但丢包 ≠ 数据错乱, 丢包只会导致部分数据缺失(如视频卡顿、语音中断),不会破坏已接收数据的正确性,数据内容错误,已被UDP校验和过滤。3、可靠的UDP协议设计从前面UDP的结构大家就知道,其实它挺简洁的,UDP既然不解决这些问题,那就应用层去弥补,说实在有更多的灵活度。UDP应用层需解决乱序、完整性、丢包三个问题就本上就比较靠谱了。比如传输协议中增加一些字段:| 序列号 (4B) | 时间戳 (4B) | 载荷长度 (2B) | 载荷数据 (N B) | 为了处理乱序、重复、丢包检测等问题。对于一些关键数据添加ACK确认与重传,一些实时数据丢包了也没关系,就不需要应答了~最好是避免IP分片,控制数据包大小≤1472字节(假设MTU=1500),大数据传输时,主动在应用层分包并添加序号。如果你加快异常时的收发效率,可以采用前向纠错(FEC),通过发送冗余数据包,允许接收端通过算法恢复部分丢失数据(如RTP协议中的FEC机制)。
最后一个bug
3 10 硬创社