说明
以下是出道 two years 的经验笔记,持续更新
笔记内容
tip1
当一个结构体变量涉及到flash 读写、通信收发(按照特定字节格式的协议)等,其数据内容不能受到默认结构体对齐填补后的影响时,需要加上 __attribute__((packed)),让其按照字节的格式对齐
tip2
解析接收数据帧的常规思路
1、先从接收缓冲区找到完整数据帧
2、通过强制类型转换获取数据帧
3、注意这里指针类型强转后,Protal_st *p = (Protal_st *)pData,假设pData执行buff[0],实际上p->data 的值是 buff[3]、buff[4]、buff[5]、buff[6]组合的一个int值赋给的,所以这里需要重新指定
p->data = (pData + offsetof(Protal_st, data)),即p->data = &buff[3]
4、注意一般来说单片机都是小端模式的,所以报文通信也必须是小端模式,即低字节在前,否则 这里的 uint16_t len = p->len 还需要高低字节互换
tip3
注意函数的参数副本概念,上一篇已经说过了
tip4
当传递函数名 myFunction 时,两种方式传 &myFunction 或 myFunction 都行,都是指传递函数名的地址
tip5
注意不要混淆,联合体和枚举一样的位置
当struct 时,上边是类型名,下边是变量
当typedef struct时,下边是类型名,其不能直接定义变量,以为typedef 是声明作用,声明一个别名
tip6
注意以下两种在链表中定义节点的方式,第一种是错误的
我的理解方式
总的来说就是,当第一种typedef 方式时,编译器在 编译 ListNote *preNote 这行时,它找不到ListNote 这个关键字,因为下一行这个 ListNote 关键字才出现;当第二种 struct 方式时,编译器在 编译 struct ListNote *preNote时,它在第一行就已经找到 struct ListNote的关键字了,即使此时struct ListNote 依旧是未完全定义,但编译是通过的
因此,再看下面两种方式,就很容易理解了
tip7
结构体成员的访问和最终表示,总体来说最终表示的是数组/变量/指针就看最后一个成员变量是什么类型的数据
tip8
注意 sizeof 宏定义的使用,sizeof 变量名、数组名、类型名返回的是占用的字节数
sizeof 地址、指针返回的是4个字节,即 int 类型
tip9
注意结构体类型,当不加 __attribute__((packed)),即按照编译器默认结构体对齐,也就是说当你使用这个结构体变量读到内存中时,编译器会自动补齐,即sizeof(结构体类型/变量) >= 成员变量占用字节大小的累积和
其实,结构体类型的对齐方式是编译阶段就已经是确定了,其需填充字节数也确定了
假如上述结构体类型在编译阶段按照 uint32_t 类型对齐,对齐结果如下,通常补齐的字节都是0x00
我的理解
由于结构体成员地址统一对齐后,其成员的访问方式通过地址偏移操作,大大提升了CPU的访问结构体成员的效率;当成员未对齐,即按照字节的偏移方式访问成员,一个字节一个字节的查询,故效率低下
tip10
取消结构体的默认对齐方式,通常两种
1、使用__attribute__((__packed__)),针对某个结构体,且只能按照1个字节对齐
2、使用宏,针对多个结构体,且能按照1、2、4、8等字节对齐
tip11
inline函数,编译阶段在函数调用的地方,将函数内容展开,后续将不需要执行函数调用的开销,缺点是代码量增大
所以用法:函数内容少,一般几行的函数,可以使用内联函数,较少函数调用的开销,提高性能
tip12 结构体和联合体
1、联合体的初始化
2、联合体的赋值
3、结构体的初始化
4、结构体的赋值
tip13、函数指针
1、普通声明,p是一个函数指针,指向的函数类型是 func
2、函数指针类型声明,cert_handler_op 是一种函数指针类型,指向 func 这种类型的函数。op 是函数指针,func 函数名本身就代表该函数的地址,故可直接赋值。
tip14、数组指针
tip15、指针数组
tip16、void * 通用指针
void 表示无类型,函数定义时表示无返回值、无参数,此时不需要加return,如下
void *p = NULL; p 是一个通用指针
tip17、回调函数
1、概念
回调函数本质上就是函数指针,那使用回调函数的时机是什么?首先理解两个角色,以及其负责的工作
1、回调函数提供者:负责提供回调函数,管控具体的回调函数的内部执行内容,但不管控回调函数的执行时机、具体的参数传递。
2、回调函数执行者:负责在适当的时机执行回调函数,传递具体的参数,不关心回调函数内部的实现
接下来,跟着应用场景去理解回调函数的使用时机、优势
2、应用场景
1、事件驱动
假设我们在应用层的模块中,此时程序执行到某个时机,需要执行驱动层的继电器闭合操作,通常我们是在代码的执行地方,调用驱动层的函数接口,如下图
那问题来了,如果我驱动层代码换了,继电器闭合的接口函数改名了,那我需要替换上述应用层调用的接口函数。也就是说,驱动层改了,应用层也需要跟着改,没法实现应用层和驱动层的解耦
1、应用层头文件
假设如图是回调函数类型、应用层上下文的结构体
2、应用层上下文实例
3、应用层注册函数
4、驱动层具体的继电器操作函数
驱动层:不管控回调函数的执行时机、具体的参数传递,只负责具体的回调函数的实现
5、应用层运行前,先注册好驱动层的继电器操作的回调函数;若驱动层的继电器操作函数变了,应用层只需替换这里:contactorA_Opt ——> contactorB_Opt
6、应用层执行继电器闭合的地方
应用层:不关心具体的回调函数的实现,只管控回调函数的执行时机、具体的参数传递
7、结论:因此,应用层和驱动层的唯一耦合在 register_event() 函数,解耦完成!
2、异步通知
假设有两个状态机A、B,主状态机A 负责整机流程,B则负责从FTP服务器下载文件。当B下载完成后,一般都通过变量标志位Flag 或 事件组标志位。当然,也可以通过回调函数,如图
1、A、B上下文
2、A、B上下文实例
3、B 提供注册函数
4、A 提供具体的通知函数
状态机A:不管控回调函数的执行时机、具体的参数传递,只负责具体的回调函数的实现
5、A 在 B 去下载文件前,注册下载完成通知函数
6、A状态机中,等待 B下载完成,waiting.....
7、B状态机中,下载完成,执行通知回调函数,通知A下载完成
状态机B:不关心具体的回调函数的实现,只管控回调函数的执行时机、具体的参数传递
8、结论:虽然解耦了,但似乎把代码复杂化了,若使用事件组标志位,一两句就搞定了,无需这么多麻烦的封装。因此,合理选择使用回调函数解耦的时机,很重要!!!
3、状态机之间数据传递
1、假设有两个状态机A、B,上下文如下;场景是A可以设置B的value,B可以获取A的status
2、A、B的上下文实例
3、A提供具体的获取status函数
4、B提供具体的设置value函数
5、A的注册回调函数
6、B的注册回调函数
7、A中设置B的value
8、B中获取A的status
4、读写Buff
假设应用层在某个事件下,需要从SD/外部Flash/模块收发缓冲区/.....读or写入一串数据。
1、上下文如下
2、上下文实例
3、驱动层提供具体的读写函数
4、读写注册函数
5、读写的执行过程