糕工的C语言开发日志 半月刊(日更版) 第二期2024/7上

一、栈与内存

1.栈溢出

        栈是逆向生长的,即栈底在高地址,栈顶在低地址,一般栈的起始边界在编译完成后就被确定,他紧挨着静态区,在静态区后开始分配。

        当栈溢出后,极大概率会爆栈,然后进行未定义行为,会侵入到静态区,在未知的情况下修改了静态区的全局和静态变量。
    单片机在运行时,函数的执行依赖于栈的存储,其中函数的存储形式或者说存储单元是栈帧,这里面包含了函数形参的实体,函数内的局部变量,函数返回地址(返回到哪个栈帧,该栈帧的指针);//静态局部是在静态区,而非栈区。
    当一个函数定义的局部变量过多可能就会导致栈溢出,但是只是对指针指向的数据进行访问和修改是不占用栈大小的,所以多使用指针,尤其是使用结构体指针避免传入和定义一个过大的实体
    栈的占用动态的,当调用完一个函数之后,该栈会被销毁,所以一般来说考虑避免栈溢出设定栈大小的主要因素是主函数main调用的单个函数栈大小和局部变量大小,以及形参个数(假设形参都尽可能以指针传递了)。
    
    举例,假设现在有一个main函数,main调用了pfunc1,pfunc1调用pfunc2,pfunc1结束后调用了pfunc3;
  

void main(void)
{
    pfunc1();
    pfunc3();
}

void pfunc1(void)
{
    pfunc2();
}

    那么栈的占用情况是这样的,
    
    第一阶段:为main分配栈帧,此时栈占用最小,此时栈占用为main栈
    第二阶段:为pfunc1分配栈帧(此时还未执行到pfunc2),此时栈占用为main栈+pfunc1栈
    第三阶段:pfunc2被调用,为pfunc2分配栈,此时栈占用为main栈+pfunc1栈+pfunc2栈
    第四阶段:pfunc2调用结束,return回pfunc1了,pfunc2栈帧销毁,此时栈占用

为main栈+pfunc1栈
    第五阶段:pfunc1调用结束,return了,pfunc1栈帧销毁,此时栈占用为main栈
    第六阶段:为pfunc3分配栈帧,此时栈占用为main栈+pfunc3栈
    第七阶段:pfunc3调用结束,销毁pfunc3栈帧, 此时栈占用为main栈
    第七阶段:main结束,所有栈帧销毁,栈回到初始化状态

二、联合体与结构体

1.union的小技巧

 union联合体访问的时候可以先访问外部名,再访问内部元素,
以便于结构清晰(尽管可以直接访问联合体内部 而不通过联合体名,
但不利于结构清晰。如下是一个全程访问的实例:

typedef struct 
{
	uint8_t id;
	 union
	{
		uint8_t status;
		 struct
		{
			uint8_t status1:1;
			uint8_t status2:1;
			uint8_t status3:1;
			uint8_t status4:1;
			uint8_t status5:1;
			uint8_t status6:1;
			uint8_t status7:1;
		}every_status_t;
	}u_allstatus_t;
}userdata_t;

userdata_t robot1;


int main()
{
    robot1.u_allstatus_t.every_status_t.status5 = 1;
    printf(" %#x",robot1.u_allstatus_t.status);
    return(0);
}

输出是0X10,%#X代表带0X的16进制输出

直接访问更快捷,但是在较大的项目中通过联合体变量名再访问的方式会让你的结构更清晰

三、外设篇

1.串口

1.双串口联调小技巧,串口助手都接设备TX,监听两端的发送,注意GND要接同一组。
同时使用Keil调试模式去观测接收缓存区,观测数据一致性。
    串口助手调试和MCU设置时,请注意匹配波特率,不匹配的波特率会导致收不到
或者收到一堆0解析不出来


2串口数据处理当中,若是要清空缓存区,建议直接清空最大值,有时清空当前写入指针的位置,
可能会使得数据刷新不全,后半段有错误的数据。

3串口配置中记得使能nvic irq,不要错误的在NVIC中将UART_IRQN使能成了UART。

4.一个串口通信的设备总是不能正确回传数据,或者出现数据重叠和丢失,首先尝试降低发送间隔,
在每个数据帧发送后等待一些时间,其次是检查接收函数是否配置正确,然后验证两设备之间的数据有效性
(发了吗?发的对吗?收到了吗?回复了吗?回复的对吗?)

5.调函数,当你的数据需要解析后作出行为,但又不由下层决定时,
此时可以设定一个回调函数结构体/数组,也可以称为回调函数表,
通过上层传入函数指针的方式注册回调函数,在解析层作出对应的行为。

6.
外设的配置大抵如此,
使能外设时钟
使能外设IO时钟

创建一个配置结构体
复位配置结构体
配置结构体参数

使能外设配置参数

使能外设中断
使能外设

四、基础知识

1.常用英语

local 局部的  Direction 方向
iden 识别符    wheel 轮子
binary  二进制/二分查找
kernel 内核

2.debug与收获

1.
变长数组在C99标准开始支持,但是CPP(C++)从未支持,C11认为这是一个可选特性。
譬如 int a=10;
int arr[a];这是可行的
int arr[a] = {0};这是不可行的,变长数组不能在定义的同时初始化

2.
在调用标准库时使用<>,自定库和芯片厂商库使用“”,尽管标准库也可以使用“”,
但有时会造成奇怪的BUG,目前测试到其中一个BUG为,当对标准库使用“”
可能会造成芯片库gd32f30x.h中(GD和STM都会出现)中某个状态枚举报错;

3.
union联合的内存对齐和占用有类似结构体的规则,当联合体内部大小不一致时,
首先union的大小至少是最大元素的大小,其次看对齐,若使用默认对齐原则,
则会查看union是否是最大数据类型(假设含有结构体,那么这个最大元素看的
不是结构体,而是结构体内部元素大小)的整数倍,如果不是则向上取整。
这种情况在使用#pragma pack(1)后有所不同,此时会进行强制对齐,
直接为实际占用而不添加空的填充字节

4.
UART输出记得配置成AF_PP,使用OUT_PP会导致不输出
当外部晶振和推荐默认值不一致时,可以通过在KEIL魔术棒C/C++界面第一行define
里宏定义正确的值来处理,注意里面的宏定义是使用逗号,来分隔的。

5.
STM32不推荐使用结构体来定义要初始化的部分然后再开始开始初始化,
因为STM32的宏定义大多是强制转化来的结构体指针,使用这种结构体数组然后for循环初始化,
可能并不能正确初始化。



3.printf与刷新显示

1.
printf("\b\b%2d", bootDelayNow);
\b\b是退格符 对应键盘的backspace 也就是删除键,所以这段代码就是为了实现覆盖
原来的最后两位数据 %2d是保持宽度不变

2.

uint32_t timCount = (uint32_t)GetSysRunTime();
uint8_t bootDelayNow = 0;
uint8_t bootDelayLast = 0;
while ((timCount < BOOT_DELAY_COUNT) && !GetKeyPressed(&serialKey))
{
	timCount = (uint32_t)GetSysRunTime();
	bootDelayNow = (BOOT_DELAY_COUNT - timCount) / 1000;
	if (bootDelayNow != bootDelayLast)
	{
		printf("\b\b%2d", bootDelayNow);
		bootDelayLast = bootDelayNow; 
	}	
} 
这里的last上次时间有很重要的作用,当时间没变化时(也就是时间切换不到1秒),
就不需要刷新显示。

五、代码规范 

1.代码规范小记

1.
许多编译器只识别31字符之前的名字,所以无论是变量还是函数名都
应当保持在3-31字节的范围,以便于审查。
2.
pp代表指针的指针前缀,h代表句柄前缀,任务应使用task函数名。
3.
函数名以特定标识(所属功能组)+名词+动词命名
使用unix风格,缩进量使用空格,只使用\n 尽可能避免使用\r\n
4.
所有变量使用前必须初始化,尤其是未定义行为的指针初始化NULL;
局部变量总是定义在使用处附近,而非函数体头部。
5.
返回值类型与函数名间隔一行
譬如
void
ble_is_open(void);
6.
避免使用逗号,定义多个变量。
譬如 int a,b;
7.
if和else的内部嵌套不应该超过两层,如有必要使用函数调用来封装或者
使用switch和case,以减少复杂性和清晰逻辑。
8.
在不影响实现的前提下,短句应当放在函数体头部,尽量使函数长句在靠后的位置。
9.
switch中的case要和break对齐缩进,保证一定有default,每个case应当都有
对应的break。
10.
避免使用“魔法数字”,使用宏定义来封装,以清晰逻辑。
11.
不可被赋值的内容应当放置在判断语句的左侧,以避免==和=的混淆。

N、附录,更新日志

2024/7/1

        开始了本期,更新了1-1,涉及关于栈的生长规则和栈溢出以及一些简单的应对方案

2024/7/2

        更新了2-1,涉及关于结构化编程的小技巧,union的访问

2024/7/3

        更新了3-1和4-1 ,补充了一些串口调试的小技巧和几个常用英语

2024/7/8

        鸽子了几天,最近没太多新鲜东西,小小的更新了一下一些4-2 debug的收获

2024/7/9

      小小的更新了一下一些4-3 关于printf和显示刷新

2024/7/10

     更新了5章,关于代码风格与规范的建议,增加了4-2 debug收获

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值