目录
故事背景
在使用Telink TLSR8258芯片做一款Zigbee产品的时候,需要将代码放到公司的编译服务器上自动编译,实现SCM自动化编译流程。
需求
做事情之前,搞清楚需求。我的需求就是配合SCM部门人员,实现代码在服务器上的自动化编译。他们搭建环境,然后告诉我,我需要从服务器读取的参数,他们可以适当修改我的代码来实现参数从服务器传递到代码中,就好像命令行传递参数,或者makefile定义C代码中的变量。
一切都基本搞定后,最后留下一个需要实现的需求,就是将服务器传递的版本号"v1.00.01" 传递到代码中进行编译。我起初以为传递进来的是个字符串,而实际上并不是,这个下面会提到。
代码中原始的对版本号的定义方式是这样的:
#define ZCL_BASIC_SW_BUILD_ID {8,'v','1','.','0','0','.','0','9'}
被定义成这个亚子,是有它的原因的,涉及到代码细节不展开讲,简单说,就是为了组Zigbee的包方便。所以,想要修改这个传递的内容形式,也应该在这个宏传递进来的地方修改,而不是到代码底层去修改组包的方式。
实现 - 误以为是字符串
Step 1. “数值+字符”–>“数值+字符串”
尝试将”数值+字符“型的数组变为”数值+字符串“型的数组。这样,只要能从makefile传递进来一个字符串,我就可以直接组合为新的包。
首先想到的方法是,直接修改宏定义为”数值+字符串“:
#define ZCL_BASIC_SW_BUILD_ID {8,"v1.00.01"}
结果,warning: initialization makes integer from pointer without a cast (用指针初始化整形,没有使用强制类型转换(cast))
那么,强转试试:
#define ZCL_BASIC_SW_BUILD_ID {8,(u8*)"v1.00.01"}
结果,warning: initialization makes integer from pointer without a cast
,一个道理,我传递的仍然是一个指针。
Step 2. “字符串指针报错”–>暴力制造“整形数组”
于是乎,暴力制造一个来自于字符串的整形数组:
char t[] = "v1.00.01";
#define ZCL_BASIC_SW_BUILD_ID {8,t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7]}
报错:error: initializer element is not constant(初始化元素不是常量)
,在这个位置:
zcl_basicAttr_t g_zcl_basicAttrs =
{
...
.swBuildId = ZCL_BASIC_SW_BUILD_ID, //Error here
};
对啊,怎么能用变量来初始化结构体呢,加个const,虽然仍是变量,但声明为常量了,也许行?(这都是C语言基础差的人能想出来的损招)
const char t[] = "v1.00.01";
#define ZCL_BASIC_SW_BUILD_ID {8,t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7]}
呵呵,这不就是自欺欺人么:error: initializer element is not constant
。
Step 3. “全局初始化”–> “使用前重赋值”(重要转折)
但是等等,我突然想到,我为什么偏偏要改人家初始化的值呢。我可以让它全局初始化之后,在代码中,在它被使用前,给它赋值啊。于是乎,我找了代码中比较靠前的一个地方,加入了如下语句:
void user_init(void)
{
const char t[] = "v1.00.01";
g_zcl_basicAttrs.swBuildId = {8,t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7]};
...
}
滑稽了,数组只有初始化的时候才能用花括号,哪个老师,哦不,哪本书教你可以用花括号给已经初始化的数组赋值了。Sorry,是我C语言不过关。
Step 4. "strcpy/strncpy"赋值(重要实现方法)
那,干脆,我拼一个数组好了,用strcpy可以拼接数组,还可以指定从数组的哪个位置开始拼:
#include "string.h"
const char t[] = "v1.00.01";
char dest[20] = {0};
strcpy(dest, t);
printf("---Dest(%d) %s\r\n", strlen(dest), dest);
我并不确定在这个系统中可以使用string.h和对应的函数,有些嵌入式系统连基本的C函数库都不全的。但是很庆幸,没有报错,看打印:
-Dest(8) v1.00.01
熟悉C库的你,可能已经想到了,strcpy并不能等效为”字符拼贴“,查一下strcpy的定义,你会发现,拼贴后的字符串,是”字符串“,也就是有结尾的NULL字符。我们原始的表达方法是纯正的”字符拼贴“不带NULL字符,这就让我想到要截断,用strncpy规定要取的字符数,而不取最后的NULL字符,对照一下strncpy确认这个函数不会自己添加NULL字符。然后,我还发现并不需要#include"string.h",可能上面有别的已经include过了。先Hard code 验证下:
const char t[] = "v1.00.01";
char dest[20] = {0};
strncpy(dest, t, 8);
printf("----Dest(%d) %s\r\n=\r\n", strlen(dest), dest);
虽然得到相同的打印,但自己心里清楚,这回没有结尾的NULL了。接下来就是将这个代码的Hard code转换为正常代码,改一点测试一下:
#define TEST_VERSION_STR "v1.00.01"
g_zcl_basicAttrs.swBuildId[0] = strlen(TEST_VERSION_STR);
strncpy(g_zcl_basicAttrs.swBuildId+1, TEST_VERSION_STR, strlen(TEST_VERSION_STR));
printf("----swId(%d): %s\r\n\r\n", strlen(g_zcl_basicAttrs.swBuildId), g_zcl_basicAttrs.swBuildId);
报错:warning: pointer targets in passing argument 1 of 'strncpy' differ in signedness,note: expected 'char *' but argument is of type 'u8 *'
,说白了就是strncpy这个函数的参数应该用char*,而不是u8*,还是强转的问题。改一下:
#define TEST_VERSION_STR "v1.00.01"
g_zcl_basicAttrs.swBuildId[0] = strlen(TEST_VERSION_STR);
strncpy((char*)g_zcl_basicAttrs.swBuildId+1, TEST_VERSION_STR, strlen(TEST_VERSION_STR));
printf("----swId(%d): %s\r\n\r\n", strlen((char*)g_zcl_basicAttrs.swBuildId), g_zcl_basicAttrs.swBuildId);
打印为:
--swId(9):v1.00.01
step 5. 整理,关联服务器参数
下面是代码的一个整理,将 TEST_VERSION_STR 这个宏,和服务器传入的 ZCL_BASIC_SW_BUILD_ID 这个宏进行关联:
#ifndef ZCL_BASIC_SW_BUILD_ID //如果服务器没有定义,则使用默认定义
#define ZCL_BASIC_SW_BUILD_ID "v1.00.01"
#endif
#define ZCL_BASIC_SW_BUILD_ID_INIT_VALUE {8,'v','1','.','0','0','.','0','9'}
zcl_basicAttr_t g_zcl_basicAttrs =
{
...
.swBuildId = ZCL_BASIC_SW_BUILD_ID_INIT_VALUE,
};
void user_init(void) {
...
g_zcl_basicAttrs.swBuildId[0] = strlen(ZCL_BASIC_SW_BUILD_ID);
strncpy((char*)g_zcl_basicAttrs.swBuildId+1, ZCL_BASIC_SW_BUILD_ID, strlen(ZCL_BASIC_SW_BUILD_ID));
//printf("----swId(%d): %s\r\n\r\n", strlen((char*)g_zcl_basicAttrs.swBuildId), g_zcl_basicAttrs.swBuildId);
...
}
好了,万事大吉,上传代码!
Buuuuuuuut!本地编译没有任何问题的代码,到服务器上编译出错了。服务器是这么说的:
21:44:00 ../apps/TRTL_Sensor/app_device.c: In function 'user_init':
21:44:00 ../apps/TRTL_Sensor/app_device.c:116:47: error: expected expression before ';' token
21:44:00 ../apps/TRTL_Sensor/app_device.c:121:74: error: expected expression before ',' token
21:44:00 ../apps/TRTL_Sensor/app_endpoint_cfg.c:62:43: error: expected expression before ',' token
step 6. 传递空 != 传递0
找到对应的116:47等的位置,发现恰好都指向 ZCL_BASIC_SW_BUILD_ID,后面的符号,根据报错,怀疑这里的宏被设置为空了。让服务器管理人员查看一下,果然是这样,在当前的build 环境下定义为空,release的时候会赋值,但是当前build不过,是不能release的!放到代码里,相当于执行了
#define ZCL_BASIC_SW_BUILD_ID
也就是定义了,但定义为空,我又一次C语言不过关地认为,我只要把代码这么修改,就可以解决:
#ifdef !(ZCL_BASIC_SW_BUILD_ID)
#define ZCL_BASIC_SW_BUILD_ID "v1.00.01"
#endif
但不是这样的!空不代表“0”也不代表“NULL”,空就是空,服务器大佬这么讲:#if 后面要有表达式,空不是表达式。(科班出身就是不一样)然后服务器大佬修改了一下服务器,给出了默认值。
但仍然编译失败了!?服务器是这么说的:
12:59:36 ../apps/TRTL_Sensor/app_endpoint_cfg.c:69:16: error: too many decimal points in number
12:59:36 ../apps/TRTL_Sensor/app_endpoint_cfg.c:69:16: error: 'v0' undeclared here (not in a function)
12:59:36 ../apps/TRTL_Sensor/app_endpoint_cfg.c:69:16: error: expected '}' before numeric constant
12:59:36 make[2]: *** [apps/TRTL_Sensor/app_endpoint_cfg.o] Error 1
多蹊跷啊,这些报错:"在数字中有太多的 十进制小数点”,“‘v0'不能识别”,“数值常量前没有花括号”
。服务器大佬见状,立刻反应过来了原因,服务器传递的是makefile中定义为 v0.00.00 的变量,而不是我想要的字符串。相当于什么呢:
#define VALUE_FROM_MK v0.00.00
而我希望的是:
#define VALUE_FROM_MK "v0.00.00"
实现 - 将没有类型的宏变为字符串
Step 1. 讨论,定方案
电联服务器大佬,给出了三种解决思路:
- 通过函数将宏中的内容一个个编入字符串;
- 在makefile中做文章;
- 下下策,修改服务器传递参数的方法
Step 2. C语言#的妙用
Buuuuuuuuttt,我竟然全都没有用。
首先,我又“C无知”的信手想要一步到位地定义了如下宏:
#ifndef ZCL_BASIC_SW_BUILD_ID
#define ZCL_BASIC_SW_BUILD_ID v1.00.01
#endif
#define TEST_VERSION "ZCL_BASIC_SW_BUILD_ID"
一看不对啊,字符串不成了“ZCL_BASIC_SW_BUILD_ID”了,愚蠢地又尝试了"\"" 的转义字符方法,和"""" 的转义字符方法,自然都是不对的。这个“添加双引号”的小功能,我就不信只有我被困住过,Baidu吧!“C 宏定义 字符串”。果然,找到了“#”相关的使用,似乎能解决这个问题,再次感谢CSDN。
于是改写code:
#ifndef ZCL_BASIC_SW_BUILD_ID
#define ZCL_BASIC_SW_BUILD_ID v1.00.01
#endif
#define STR1(R) #R
#define STR2(R) STR1(R)
#define ZCL_BASIC_SW_VERSION STR2(ZCL_BASIC_SW_BUILD_ID)
void user_init(void) {
g_zcl_basicAttrs.swBuildId[0] = strlen(ZCL_BASIC_SW_VERSION);
strncpy((char*)g_zcl_basicAttrs.swBuildId+1, ZCL_BASIC_SW_VERSION, strlen(ZCL_BASIC_SW_VERSION));
}
编译,烧录,验证OK,终于顺利Release!
Step 3. 知其然,更要知其所以然
C语言“#”这一使用方法,可谓“妙用”,搞懂它! 找一篇网文学一下就好。
总结
这一切的不顺利,全都怪C语言基础太差,走了很多弯路,换做C大神,可能一想就知道走错了,哪还会“试来试去”的。
但这一切又顺利解决了,蛮有成就感,可能源于坐的住,相信问题总能解决,只要磨洋工,啊不,只要肯钻研。想到当时笔试E某大厂,猎头说我是他这个月介绍的所有人中唯一一个笔试通过的,我都没想过自己能过,2小时2道题的笔试,后面的题是汽车拍牌系统的建模,读题都很难读懂,我愣是一动不动一点一点把整个模型实现了。我觉得真正让那些选手被淘汰的是不自信,不认为自己有能力解决这个读都读不懂的题,于是觉得系统很复杂。我一个以上C语言基础,写代码靠试靠蒙的人,凭什么能比得上那些准备良多的C大神?可能就是坐的住吧。