嵌入式开发易错点及开发技巧总结(持续更新)
目录
前言
本篇内容主要列举一些自己在嵌入式开发过程中踩过的大坑(还有平时收集的一些易错点),希望能对遇到类似问题的开发者有所启发。
易错点总结
1、关于浮点数比较及交换
第一次碰到这个问题是在刚毕业面试那会,当时有一个面试题:怎么判断一个float类型数据是否为0?一开始看到这个问题的时候还真没当回事,不就 return (f == 0);就完事了吗?结果可想而知。现在回过头来看,确实这个回答略显业余。
void main()
{
float x =1.2; float y = 3.2;
if((x + y)==4.4) { printf(“correct\n”); }
else { printf(“ error \n”); }
}
结果是error!!!浮点数是为了能用二进制表达小数而引入的一种量化形式,转换时存在舍入差,因此不能直接用“==”比较浮点数,正确的做法应该是确定一个误差范围,通过比较两个浮点数的差值是否在这个误差范围内来判断是否相等。即
const float delta = 0.1
void main()
{
float x =1.2; float y = 3.2;
float z = x + y;
if((z < (4.4+delta))&&(z > (4.4-delta))) { printf(“correct\n”); }
else { printf(“ error \n”); }
}
那么很显然判断一个float数据是否等于0应该是如下表达
int isZero(float data)
{
//float类型一般精确到小数点后5位或6位
return(data > -0.000001 && data < 0.000001);
}
2、关于strlen()使用
如下列代码所示
/*判断一*/
if(strlen(x)>= strlen(y))
{
}
/*判断二*/
if(strlen(x)- strlen(y)>= 0)
{
}
从表面上看,上面的两个判断表达式“完全相等”,但实际情况并非如此。其中,判断表达式一没什么问题,程序也能够完全按照预想的那样工作; 但判断表达式二的结果就不一样了,它将永远是真,这是为什么呢? 原因很简单,因为函数 strlen 的返回结果是 size_t 类型(即无符号整型),而 size_t 类型绝不可能是负的。因此,语句“if(strlen(x)-strlen(y)>=0)”将永远为真。
3、关于自动转换规则
这个问题是在几天前碰到的,类似代码如下
unsigned char foo(void)
{
unsigned int a = 6;
int b = -20;
return (a+b > 6);
}
以上函数返回值为真!原因:当表达式中同时存在有符号类型和无符号类型时,所有的操作数都自动转换为无符号类型,上式中-20会被自动转换为一个非常大的正整数,所以结果会远远大于6。(如果这个问题出现在上大学那会,我想肯定不会出错,其实很多问题的解决方法往往是最基础的理论知识,像运算符优先级、自动转换这些概念往往容易出现问题,但却都是最基础的代码规则,所以大家在写代码时切勿眼高手低,遇到拿不准的地方及时巩固)
4、关于数据类型长度
还是一个面试,当时面试官问我:int类型数据占用几个字节?4个字节!接着他问我确定吗?我犹豫了,因为我觉得他不会问这么简单的问题!那是不是就要分情况分析?不同编译器、不同位数的处理器是不是就不一样?我们常用的处理器都是32位,int类型默认占用4个字节,那如果是64位处理器是不是就是8个字节?float类型是不是也会有变化?究其原因还是对底层概念的模糊。
那么今天在这里做一个简单的归纳:数据类型的长度与编译器规则密不可分,尽管同样的类型在不同位数的系统上所代表的位数也可能不同,但是总归是有一些规则约束的。编译器可以根据自身硬件来选择合适的大小,但是需要满足约束:short和int型至少为16位,long型至少为32位,并且short型长度不能超过int型,而int型不能超过long型。这即是说各个类型的变量长度是由编译器来决定的,下面列举在GCC编译器下16位机器、32位机器和64位机器各个类型变量所占字节数:
| C类型 | 16 | 32 | 64 |
|---|---|---|---|
| char | 1 | 1 | 1 |
| short int | 2 | 2 | 2 |
| int | 2 | 4 | 4 |
| long long | 8 | 8 | 8 |
| char* | 2 | 4 | 8 |
| float | 4 | 4 | 4 |
| double | 8 | 8 | 8 |
5、关于字长
字长涉及到的易错点有很多,数据的字节对齐、结构体数据的步进值、指针移位等等,但这些相信大家平时接触的都比较多,这里不再详细列举,请看下列示例
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
如上代码,在一个int类型不是16位的处理器中表达就是不正确的,应该表示为
unsigned int zero = 0;
unsigned int compzero = ~0;
/*1's complement of zero */
(在单片机中,常采用“一补”(1's complement)格式,也即在16位处理器中,0xffff就是0,在32位单片机中,0xffffffff就是0;在手机端或者电脑端一般采用“二补”(2's complement),即0xffffffff就是-1)
6、关于续行符
续行符'\' 用于将宏定义、字符串或代码跨行书写,提升可读性。
核心规则:'\' 必须是行的最后一个字符,后面不能有任何空格、制表符或注释,否则会导致预处理错误。
正确用法:
#define ADD(a, b) \
((a) + (b)) // \后直接换行,无空格/注释
int sum = ADD(3, 4); // 展开为 ((3) + (4))
错误用法:
#define ADD(a, b) \ //a+b 错误!!! 续行符后不能追加注释或空格
((a) + (b))
int sum = ADD(3, 4); // 展开为 ((3) + (4))
特殊场景处理:注释与续行符共存,需要把注释写在续行符前面。
#define ADD(a, b) /* 注释写在\前 */ \
((a) + (b))
7、bool类型与“零值”判断
乍一看是不是很简单?是的,但你想过深层的逻辑没?
bool b;
if(b == 1)
if(b == TRUE)
以上这个答案是有问题的!为什么?首先要清楚什么bool类型?什么是真?什么是假?
很简单,0是假,非0是真!!!
那么,什么叫非0?-1,算不算,1算不算,10000算不算?都算!反正不是0就是真。
所以,如果有个值i = 2,那么if(i == TRUE)就不灵了。正确的应该这样:
bool b;
if(b)
if(!b)
8、指针常见大坑
8.1指针传递
请问下列代码有什么问题?
void getptr(char *ptr)
{
ptr = (char *)malloc(100);
}
void test(void)
{
char *ptr = null;
getptr(ptr);
strcpy(ptr, "hello");
printf("%s", ptr);
free(ptr);
}
乍一看似乎没什么问题,但总觉得哪里有点奇怪。
指针是什么?
指针本质上是一个变量,用于存储内存地址。
问题分析:
void test(void) {
char *ptr = NULL; // ptr变量存储在test的栈帧中
getptr(ptr); // 传递的是ptr的值(NULL)的副本
// 这里ptr仍然是NULL!
// strcpy将尝试向地址0写入数据 → 段错误
}
如果非要使用指针传递,则应该写成这样
void getptr(char **pptr) // pptr是指向指针的指针
{
*pptr = malloc(100); // *pptr就是原ptr变量
}
void test(void)
{
char *ptr = NULL;
getptr(&ptr); // 传递ptr的地址
// 现在ptr指向有效内存
}
不妨再来看一个更直观的例子:
void cannot_change(int x)
{
x = 100; // 只修改副本
printf("函数内: x = %d\n", x); // 输出 100
}
void can_change(int *x)
{
*x = 100; // 通过地址修改原变量
printf("函数内: *x = %d\n", *x); // 输出 100
}
void test(void)
{
int a = 10;
printf("调用前: a = %d\n", a); // 输出 10
cannot_change(a);
printf("调用后: a = %d\n", a); // 输出 10(没变!)
can_change(&a);
printf("调用后: a = %d\n", a); // 输出 100(变了!)
}
这样是不是就豁然开朗了?!所以,不要看到数据带星号就开始发懵,想想清楚本质,你就会发现并没有什么大不了事情。
总结:
-
指针也是变量,它存储的是地址值
-
C语言总是按值传递,包括指针本身
-
要修改函数外的变量(包括指针变量),必须传递它的地址
-
对于指针变量来说:
-
要修改指针指向的内容 → 传递指针
char *ptr -
要修改指针本身(让它指向新的地方)→ 传递指针的地址
char **ptr
-
开发技巧总结
1、数据取余
value % x 等同于 value & (x-1),仅限于对偶数取余
2、宏的高级用法
使用场景:假如我们有很多种动物(猫,狗,鸡等等),都需想用一个结构体来定义,再假如结构体都只想描述动物的重量和身高,但又不想所有动物的结构体名称一致。假如结构体都想用Animal_打头,如Animl_Cat,同样我们用宏定义来实现:
#define StructureType(type) typedef struct{ \
float weight; \
float height; \
}Animal_ ## type;
使用:
StructureType(Cat);// 定义一个Animl_Cat结构体
Animal_Cat cat;// 实例化一个Animl_Cat
cat.weight = 10;
cat.height = 30;
你可能在想,我想定义一个新的动物结构体,增加一个动物属性,那就结合上面的组合方式
#define StructureTypeEx(type, member) typedef struct { \
float weight; \
float height; \
char* ## member; \
}Animal_ ## type;
使用:
StructureTypeEx(Dog, Name);// 定义了一个动物名
Animal_Dog dog;
dog.height = 60;
dog.weight = 100;
dog.Name = (char *)malloc(32);
memset(dog.Name, 0, 32);
strcpy(dog.Name, 7, "HuaHua");
3、临界区保护(裸机)
裸机中的临界区保护实现,分享常用的两种方式。
方式1:直接调用ARM内核接口,简单粗暴。
uint32_t enter_critical(void)
{
// 保存当前 PRIMASK 值
uint32_t regPrimask = __get_PRIMASK();
// 关闭系统全局中断(其实就是将 PRIMASK 设为 1)
__disable_irq();
return regPrimask;
}
void exit_critical(uint32_t primask)
{
// 恢复 PRIMASK
__set_PRIMASK(primask);
}
方式2:使用汇编实现,使用时需要定义一个uing32_t的变量用来保存中断状态。
//用于临界态
__asm unsigned int OS_CPU_SR_Save(void)
{
MRS R0, PRIMASK ; Read PRIMASK to R0
CPSID I ;Disable interrupts and configurable fault handlers (set PRIMASK)
BX LR ;Return from function call
}
__asm void OS_CPU_SR_Restore(unsigned int Arg)
{
MSR PRIMASK, R0
BX LR
}
//调用
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();} /* disable interrupt*/
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);} /* enable interrupt*/
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)