1.软件问题从哪来
软件缺陷问题千千万万,主要是需求、实现、和运行环境三方面。
1.1 需求描述偏差
客户角度的描述,在经过业务对接、产品经理的转述,最终呈现的软件需求可能已经偏离了原始的述求,开发人员基于自身经验的理解偏差,开发过程缺乏有效的沟通及监督,导致最终的软件功能与客户的核心诉求存在偏差。
1.2 异常处理机制不完善
嵌入式软件必定是运行在特定的硬件设备,硬件本身或环境问题等特殊干扰,开发人员因经验不足缺乏风险评估,面对电脑是无法全方位猜测、模拟各种异常环境下的差异,最终导致设备在特定场景下运行异常。
1.3 软件开发能力不足
嵌入式系统的复杂度与开发人员的能力矛盾,导致软件本身的逻辑存在缺陷。
2.软件开发与软件问题
关于软件bug的来源,排除不可控的外界因素,与软件开发人员相关,或者开发人员可以减少问题的发生的可能,从软件开发角度解决的方案如下:
2.1 重视需求分析
软件开发就是写程序,并设法使之运行,这是个错误的想法。软件结果与客户期望不一致,需求问题不全是软件开发的锅。大多数情况下客户的原始述求不会直接到软件开发,软件开发没法去反诉找客户确认,只能通过软件的实现形式去甄别不合理的,或者针对客观环境、研发团队的基础去评估风险。
比如客户要求可以设备可以定时1秒采集一次温度,精度要求0.0001摄氏度;或者要求数据采集持续采集24h后,每天12:00准点TCP上报后台服务器。这就需要考虑温度传感器的精度、RTC唤醒以及TCP联网时间、24小时采样数据的存储。如果硬件资源或者客观环境无法实现,盲目承诺客户,或者开始编码,最终结果可想而知。
软件开发是个人的任务,但开发前多沟通确认,进行风险评估反馈,减少开发的无用功,也是对开发人员的基本要求。
2.2 积累行业经验
嵌入式产品都是针对某个细分行业,见多识广,才能预判可能出现的异常,开发阶段有针对性的去处理,或者提前告知使用者去规避。有时候经验比技术能力重要。
2.3 提高开发水平
软件开发水平,首先是个人能力,熟悉软件SDK的应用,相关的操作系统、设计模式、调试方法等。软件开发能力大多数情况下决定软件质量和可维护性,这是个长期学习提高的过程,如果一定要提供捷径,那就是多阅读优秀的开源代码。
2.4 先设计再编码
软件开发不能随心所欲,先明确方案和大概的实现流程,胸有成竹,然后再开始编码,完善细节。这理论没毛病,但真正执行起来却比较难,大多数情况下都只在乎软件出结果,而实际上方案不合理,后期修修补补更浪费时间。如果制度和时间不允许,个人在纸上画画框图和结构,先构思再开发也能弥补,起码不至于南辕北辙。
2.5 编码规范
编码规范是软件开发团队合作的标准,嵌入式行业可以参考“华为技术C语言编程规范”,但实际开发过程,和前面的先设计再编码一样,各种不可控因素,比如项目进度压力和开发者水平与认知的差异,导致有编码规范却无法严格执行。随着软件工程规模的扩大,软件交期、代码同步、重构或交接,其风险也逐渐放大。存在编码规则并不能解决问题,只有强制执行才有意义。
2.6 代码缺陷静态检查与单元测试
软件质量是项目成败的关键点之一,在开发周期有限,人力资源不足的情况下,使用工具实现代码自动扫描,分析出潜在隐患点,可从源头减少软件bug,比如cppCheck、PC-lint等,实现代码自动静态分析,或者人工视检,有效规避简单的软件风险。
如果可能,最佳的选择是单元测试,单元测试比可交付成果本身更重要,文档注释不全时,单元测试就是设计文档;单元测试定义的API和用法,以及可能的使用风险点,就是最佳的参考范例;不足100%的覆盖率就是玩忽职守,开发人员应该全权负责测试自己造出的产品。依靠后期的黑盒测试发现问题,其消耗的人力物力,是编写单元测试的几倍,而且单元测试可以反复的自动测试。不过这种情况更多的是存在于开发理论中。可以参考微信公众号 嵌入式系统 的《代码的保养》第二章。
3.前期减少问题
软件问题的解决,有些不是个人能解决的,需要协调沟通,或者与研发团队的整体风格、制度有关。个人能决定的是软件具体逻辑,这也是体现个人技术能力的重点。
3.1 C语言基础
- 多看优秀代码,学习其技巧。
- 使用带参数检测的接口,比如优先选择snprintf,少用sprintf,其它str前缀的如strncmp也是,但要明白这类接口和memcmp区别。不同的编译器表现不一致,平时也要多关注。
在GCC中编译运行(设备):
char str[5];
int ret = snprintf(str, 3, "%s", "abcdefg");
//ret = 7 ,str = ab
char str[99];
int ret = snprintf(str, 99, "%s", "abcdefg");
//ret = 7 ,str = abcdefg
注:snprintf的返回值为字符串的长度,且写入的字符串后面带有‘\0’结束符。
在VC中编译运行:
char str[5];
int ret = snprintf(str, 3, "%s", "abcdefg");
//ret = -1 ,str = abc [后面不会自动补\0结束符]
- 注意函数返回类型,避免类型强制转换导致调用判断异常,有些编译器对隐示类型转换直接报错,因为它确实存在风险。
- 合理的使用sizeof、struct、union、weak等关键字,增加代码的可读性和可扩展性。
- 参数使用前,如数组小标,指针变量使用前必须先判断是否合法。
- 浮点数不能直接进行==和!=比较,等等,这些细节太多,可以参考《C陷阱与缺陷》。
- 讲的都会,说的都对,但真实际写代码,就容易各种小问题,主要还是态度问题,缺乏自我检查、自测的步骤,依靠测试发现bug去驱动研发调试修复是大忌。
3.2 动态内存
- 尽量做到申请与释放在同一个函数,申请内存后,先判断是否申请成功,再进行其它操作。
- 内存申请与释放之间有特殊情况return,要注意释放。
- 释放结构体指针前,注意该变量内部是否还有指针变量动态申请空间,先释放内部,再释放外部。
- 关于内存申请与释放,或使用越界是C语言的劣势,如果设备堆空间足够大,可以在申请时额外多申请固定空间,记录申请函数、长度、并在首尾标记,后续释放时检查内存区首尾标记是否被覆盖;或者查询是哪些函数申请的内存始终没有被释放。
3.3 跨平台问题
- 使用系统API前先判断自身传入参数的有效性和范围等是否符合要求,一般系统API是库文件,使用错误更难发现问题。
- 针对不同的平台常用的接口,务必增加适配层隔离,便于调试和后续移植。比如有的平台中断(SDK提供的中断回调不一定是硬件中断)不支持串口日志。
3.4RTOS系统特性
- 多任务的竞争,在RTOS系统中,需要注意全局函数、全局变量的使用,避免互相竞争影响,对公共函数尽量做到可重入设计,具体实现方案请关注微信公众号 嵌入式系统 的《基于RTOS的软件开发理论》 。
- 中断与任务的调度关系 请关注微信公众号 嵌入式系统 的《基于RTOS的软件开发理论》。
- 合理分配任务栈空间和消息队列的深度,函数内部尽量少用大数组。
3.5 个人素养
软件编码完成,不是能编译就收工了,其功能是否符合预期,开发人员自己检查是最高效的,很多问题都是开发不仔细,或者很简单的C基础应用错误,这不是技术问题而是心态。可以多看看开源代码,或者《C专家编程》等。
4.后期解决问题
如果软件问题不可避免,该如何去修复解决呢?
一般来说100%出现的问题都比较容易解决,找到相关代码仔细检查或者加点日志就能发现问题。难处理的是小概率出现的问题,稳定复现它就是成功的一半。
4.1 问题复现
稳定复现问题才能快速对问题进行定位、解决以及验证,如何提高复现的概率?
- 模拟复现条件,问题只在特定的条件下出现,对于依赖外部输入的条件难以满足,可以考虑程序里预设直接进入对应状态,或者软件内部进行极端的压力测试。
- 提高相关代码执行频率,进行某个操作才可能出现异常,人工持续操作,或者软件频繁执行相应的功能,提高问题点的执行频率,加快复现速度。
- 增大测试样本量 ,个别样机难出现,如果条件允许,可以使用多个设备同时进行测试。一般情况下试产就是为了发现这类问题。
4.2 问题定位
缩小排查范围,确认引入问题的函数或代码片段。
- 打印日志 ,日志是最直接、简单的调试方法,在问题的可疑点增加日志输出,以此来追踪程序执行流程以及关键变量的值,观察是否与预期相符。
- 版本回退,使用版本管理工具时可以通过不断回退版本,验证前面版本的情况,定位首次引入该问题的版本,针对该版本的改动进行排查。
- 二分注释,“二分注释”类似二分查找法的方式注释掉部分代码,以此判断问题是否由注释掉的这部分代码引起。具体为将与问题不相干的部分代码注释掉一半,看问题是否解决,未解决则注释另一半,如果解决则继续将注释范围缩小一半,以此类推逐渐缩小问题的范围,确定是哪一块代码导致这个问题。
- 硬件协助,借助示波器、逻辑分析仪分析波形,必要时也请硬件协助分析;问题样机与正常样机的主控对调,看问题是否随芯片走。尤其是涉及驱动方面的问题,比如充电、中断、复位、外设通信调试异常时。
- 仿真调试 ,在线调试可以起到和打印LOG类似的作用,适合排查程序崩溃类的BUG,当程序陷入异常中断候可以直接STOP查看call stack以及内核寄存器的值,快速定位问题点,不过这需要硬件支持。
- 三板斧,使用最多的是前面三种方法,这三板斧足以应付大部分业务逻辑问题;偶尔请硬件协助解决驱动问题,日常开发中的问题都能解决。个别系统层面或者架构不合理导致的深沉问题,要么花时间死磕coredunmp,要么联系原厂FAE协助,一般芯片方案商都提供技术支持。
4.3 问题修复与回归测试
- 缩小范围确定问题代码,再排查具体的函数,修复问题点。
- 有些问题属于架构层面,比如和RTOS相关的竞争关系,这种就无法定位到具体问题代码点,只能在宏观上依靠经验或操作系统理论去解决。
- 解决后需要进行回归测试,确认问题是否不再出现,也要确认修改不会引入其他新问题。
4.4 复盘
- 一般情况下最后发现原因都是很简单的几句话,比如数据越界或者循环体多执行一次,看起来都是很简单的基础用法,因为一句错误可能需要几周时间来发现解决,为什么当初写错而且没检查发现呢?
- 总结问题产生的原因及解决方法,今后如何防范,对其他平台否值得借鉴,做到举一反三,从失败中吸取经验。
5.心得
业务指示开发、测试驱动开发,这一荒谬方法论,体现在部门合作与职责不清,整体就是效率低下、互相推诿,在这样的环境下开发软件也很累。