• 从PLC到C#上位机:我的转型心得与成长蜕变

    作为一名拥有5年PLC编程与自动化调试经验的工程师,我曾长期深耕工业现场,熟悉梯形图、结构化文本语言(ST)、功能块的逻辑搭建,也能熟练处理串口、以太网等通信问题。但随着工作年限增长,我逐渐陷入职业瓶颈——工作内容多是重复性的设备调试与程序修改,加班频繁,出差频率高,薪资增长缓慢,发展边界清晰可见。 一次偶然的机会,我与客户外包的上位机软件工程师对接项目,上位机软件工程师根本不需要来客户现场,听说收费还比较高,对接现场的问题都是我出来,我心里想,我一个人把PLC控制+上位机软件开发的工作全部搞定,公司是否会给我加薪,或者我就可以帮客户做同样的外包工作。 通过我接触到C#上位机开发,发现这正是突破瓶颈的关键,如今转型成功已一年有余,回望这段旅程,满是感悟与收获,在此分享给同样迷茫的PLC同行。 一、转型初期: 打破恐惧,找准衔接点是关键 刚开始考虑转型时,我和很多同行一样充满顾虑:没有软件开发基础,能学会C#吗?软件编程逻辑和PLC逻辑差异大,能适应吗?但真正深入了解后才发现,PLC工程师转型C#上位机,其实有着得天独厚的优势,完全不用从零开始。 我最大的感悟是“立足工业经验,衔接软件技能”。比如在学习C#基础语法过程中,发现数据类型差不多,控制语句IF,FOR也类似。通过了解,PLC的低级语言也是类似C语言实现底层开发,所以跟高级语言比较接近。比如在学习C#通信编程时,我发现Modbus、TCP/IP等协议正是我平时PLC工作中频繁接触的,之前积累的硬件交互经验直接派上了用场。我不再是机械记忆代码,而是能结合工业现场场景理解“软件如何与PLC、单片机、运动控制器、传感器实现数据交互”,这种衔接让我入门速度远超零基础学习者。 初期最需要克服的是“思维转变”——从PLC的“线性控制逻辑”面向过程转向C#的“面向对象编程思维”。我没有盲目啃厚厚的理论书籍,而是选择从工业场景切入,先学习如何用C#实现简单的设备数据采集与显示,再逐步深入WinForm界面设计、数据库操作。这种“边用边学”的方式让我快速建立了信心,也避免了陷入理论冗余的困境。 二、转型过程: 深耕实战 把经验转化为核心竞争力 如果说入门靠的是工业经验衔接,那真正站稳脚跟则要靠“实战积累”。作为PLC工程师,我们最宝贵的财富就是对工业场景的深刻理解,这也是我们转型后区别于纯软件工程师的核心优势、上位机主要是控制PLC,逻辑控制相对比较少,运动控制卡的逻辑控制基本上在上位机实现。 在学习过程中,我始终围绕工业需求展开实战。比如在做生产线监控系统项目时,我能精准把握车间操作人员的使用习惯——界面要简洁直观、数据展示要清晰、报警功能要及时准确,这些基于现场经验的判断,让我开发的软件更贴合实际需求,避免了“纸上谈兵”。而这些细节,往往是没有工业经验的软件工程师难以兼顾的。 此外,我特别注重“经验复用与技能融合”。之前在PLC工作中积累的工业控制逻辑、流程优化思路,都能在上位机开发中发挥作用。比如在设计生产数据统计功能时,我能结合生产线的实际流程,精准筛选出关键数据指标,设计出更合理的数据统计模型。这种“硬件+软件”的复合能力,让我在项目中逐渐凸显价值。 三、转型收获: 不止是薪资提升更是职业边界的拓宽 首先是薪资的显著提升。转型前我的月薪稳定在12K左右,转型后凭借上位机开发技能,薪资直接提升至20K,涨幅远超之前的预期。这也印证了市场对“工业+软件”复合型人才的稀缺需求,我们的价值终于得到了更充分的体现。 更重要的是职业赛道的拓宽。以前我只专注于PLC设备层面的工作,转型后我能参与到整个自动化系统的架构设计中,从上位机软件开发、数据可视化搭建,到与MES系统对接,工作内容更具挑战性和成长性。我不再是单一的“设备控制者”,而是成为了能统筹“硬件+软件”的“系统架构者”。 还有个人能力的全面跃迁。通过学习C#、.NET框架、数据库设计等技能,我的技术体系更加完整,解决问题的思路也更开阔。面对工业现场的复杂问题,我既能从PLC硬件层面排查故障,也能通过上位机软件优化控制逻辑,这种综合能力让我成为企业不可或缺的核心技术人才。 四、给同行的建议: 转型不是跨界,而是升级 很多PLC同行担心转型门槛高,其实我想说:PLC到C#上位机,不是跨界,而是职业的自然升级与延伸。如果你也符合这些条件,不妨勇敢尝试:从事PLC工作1年以上,熟悉工业控制逻辑;渴望突破职业瓶颈,提升薪资;对软件开发有兴趣,看好智能制造的前景。 最后给大家两个转型小技巧:一是“精准学习,拒绝盲目”,优先学习与工业场景强相关的技能,如C#通信编程、WinForm界面开发,避免陷入基础理论的冗余学习;二是“边学边练,立足实战”,多参与真实工业项目,把PLC经验转化为上位机开发的核心竞争力。 工业4.0浪潮下,智能制造对复合型人才的需求越来越迫切。从PLC到C#上位机的转型,让我搭上了行业发展的快车,实现了职业价值的翻倍。希望我的心得能给迷茫中的你一点启发,勇敢迈出转型的第一步,你会发现更广阔的职业天地!

    01-20 336浏览
  • PLC上位机软件工程师的接活儿指南!

    前段时间主要跟大家分享的是上位机开发的一些技术主题,今天我们聊聊工程师们关注的另一个话题——接活儿。 作为一名资深的C#上位机软件工程师,在完成公司自动化设备软件开发工作的基础上,可依托自身积累的人脉与技术资源,逐步延伸业务范围,承接自动化领域的软件开发外包项目。当前,许多企业因控制人力成本,未配备专门的上位机软件工程师,但受客户项目特殊性要求,又必须通过上位机软件实现与客户系统的集成。 随着工业物联网、工业4.0的普及,单一“触摸屏+PLC”模式已难以适应时代需求,行业对自动化系统提出了更高要求,“上位机(PC)+触摸屏+PLC”的组合模式愈发受到企业青睐。 承接外包项目是提升实战能力、增加收入的重要途径,尤其适合转型期的工程师——既能积累工业场景项目经验,又能快速深化PLC与上位机的技术融合(你的PLC背景是核心优势)。 以下将结合外包全流程、工业上位机特色、实战案例及避坑指南,系统分享承接外包项目的经历与经验,助力同行少走弯路。 一、外包项目的核心特点 (工业上位机专属) 和普通软件外包不同,工业上位机外包有明显的行业属性,需提前明确:每个项目对应不同设备(PLC 型号、单片机、运动控制器品牌)、不同工厂流程,无通用模板,需现场调研或深度对接甲方工程师。需兼容多种 PLC(西门子、三菱、欧姆龙、汇川等)、通讯协议(Modbus、OPC UA、TCP/IP、MC,S7,MQTT),常遇到协议不匹配、数据丢包问题。工业场景需 7×24 小时运行,数据采集不能中断,需考虑断电恢复、异常重试、日志记录等机制。部分甲方是工厂电工 / 技术员(只懂设备不懂软件),部分是技术团队(要求细节多),沟通成本差异大。需提供可运行软件、源代码(部分项目)、操作手册、通讯调试报告,部分要求现场安装调试。 我们接单基本上是熟人推荐、包括前同事、PLC 客户、行业人脉。但是我们得把前期需求明确、一定要明确需求,不能后面会非常麻烦,然后报价。也不一定要去现场,写好操作文件,使用手册。 二、开发的几个阶段 (三个阶段) 阶段 1:通讯调试(最耗时,占比 30%): 若甲方能提供 PLC 模拟器或远程访问权限,先在本地调试通讯(用我们提供的5个常用工具基本上可以满足验证测试功能); 若需现场调试,提前和甲方约定时间(工业生产多在夜间 / 周末停机时),带齐设备(笔记本、串口线、交换机、调试软件)。 避坑点:部分甲方 PLC 端口未开放、寄存器地址写错,需现场排查(你的 PLC 背景能快速定位问题,这是核心竞争力)。 阶段 2:功能开发(占比 40%): 优先开发核心功能(数据采集 + 监控界面),再做辅助功能(报表、报警); 工业场景必备功能:数据缓存(断网时存储本地,联网后同步)、异常日志(记录通讯失败、数据超量程等)、权限管理(管理员 / 操作员账号)。 工具推荐:用 DevExpress 快速搭建工业风界面,用 NLog 记录日志,用 EasyModbus 库简化 Modbus 通讯代码。 阶段 3:测试优化(占比 20%): 压力测试:模拟 1000 条数据连续采集 12 小时,检查是否卡顿、数据丢失; 兼容性测试:在甲方指定的 Windows 系统上测试(尤其是老设备用 Windows XP,需注意.NET Framework 版本兼容); 甲方联调:让甲方操作软件,提出修改意见(比如界面布局调整、参数显示格式优化),避免验收时返工。 三、外包项目实战案例复盘 (3 个典型场景) 四、接外包的利弊与转型期建议 (优势及劣势分享) 1. 优势 实战经验积累快:覆盖不同行业(化工、食品、机械)、不同 PLC 型号,技术全面性远超固定工作; 收入弹性大:成熟后单价可达 100-300 元 / 小时,月入 2-5 万常见; 自主安排时间:可兼顾转型期学习与接单,灵活调整节奏。 2. 风险与避坑 需求变更:合同中明确 “需求变更需额外收费”,避免无偿返工; 技术风险:接项目前评估自身能力,不接超出技术范围的项目(比如复杂运动控制、定制协议开发); 回款风险:优先接预付款项目,不接 “先开发后付款” 的单子; 时间管理:避免多项目并行导致工期延误,建议同时承接不超过 2 个项目。 3. 转型期接外包的优先级建议 初期(1-3 个月):接 “低难度 + 短工期” 项目(比如单设备数据采集、参数设置),积累案例和口碑; 中期(3-6 个月):承接 “中难度 + 行业特色” 项目(比如生产线联动监控),强化工业场景优势; 长期(6 个月以上):聚焦垂直领域(比如新能源、智能制造),打造个人品牌,接高单价项目。 五、总结: 对于 PLC 转型 C# 上位机的工程师来说,接外包是 “技术落地 + 收入提升 + 行业资源积累” 的三赢选择。核心优势在于你懂工业场景、懂 PLC 通讯,能解决纯软件工程师搞不定的现场问题 —— 这是你接单的 “护城河”。 初期重点是 “稳”(接小项目练手,积累案例),中期重点是 “专”(聚焦工业垂直领域),长期可考虑组建小团队承接更大项目。记住:外包不仅是赚钱,更是转型期快速提升实战能力、验证技术价值的最佳途径。祝你接单顺利,转型成功!

    01-19 580浏览
  • PLC工程师转上位机,最该先补哪块知识?

    这阵子连续发了好几篇关于PLC工程师上位机开发的实用文章,比如《上位机与PLC如何实现数据交互?只需四步!》大家就很感兴趣,接下来,我们会有更多验证过的实用技术给大家分享。这些话题也会在我们的开发群中讨论,群已经满了几个了,大家要进群的赶紧文末扫码。 PLC 工程师转上位机开发,核心优势是懂工业场景、懂 PLC 通讯逻辑、懂控制需求,短板集中在软件开发思维、界面开发、数据处理三大核心模块。优先补的知识要围绕 “快速落地工业场景” 展开,按“优先级从高到低”排序,确保学完就能用,具体如下: 第一优先级 (核心刚需,学完就能对接 PLC 做基础上位机) 1. 工业通讯协议,如 Modbus RTU\TCP),主要针对上位机如何主动读写 PLC 数据,这是上位机和 PLC 交互的核心,必须先吃透: 重点掌握:Modbus(RTU/TCP):上位机侧的读写逻辑(如读取 PLC 寄存器、下发指令),推荐先学 Modbus TCP(以太网版,最通用),比如用 C# 的 HslCommunication库模块实操; 品牌专属协议:如果常对接西门子 PLC,上位机主要实现(如 C# 的S7.Net库);对接三菱补 MC 协议。 实操方式:用自己熟悉的 PLC(比如西门子 S7-1200),搭建上位机 + PLC 的最小通讯环境:上位机读取 PLC 的 I/Q/DB 块数据,下发启停指令,验证数据交互的准确性。 2. 基础的界面开发(工业上位机只需要、能用、稳定)PLC 工程师不需要做复杂 UI,但要掌握 “工业级界面” 的核心能力 —— 数据可视化、数据存储、简单交互的框架: 优先学 C#基础语言、 WinForm(80% 工业上位机的选择,适配 Windows 工控机,拖拽式开发,效率高); 核心控件:按钮(下发指令)、文本框 / 标签(显示 PLC 数据)、仪表盘 / 趋势图(显示温度 / 转速等模拟量)、报警控件(异常提示); 关键技能:控件和 PLC 数据的 “绑定”(比如文本框实时显示 PLC 的 DB2.DBD10 数值)、界面刷新(避免卡顿,比如用定时器 / 多线程读取 PLC 数据); 实操目标: 做一个极简上位机界面:显示 PLC 的 3 个数据(如电机转速、IO状态、数量),1 个启动 / 停止按钮下发指令,1 个趋势图显示转速变化。 3. 数据存储基础(工业上位机必备:记录生产数据 / 故障日志) PLC 工程师懂 PLC 的临时存储,但上位机需要持久化存储数据,优先学 “轻量、易上手” 的方案,不用一上来就学大型数据库: 重点掌握: SQLite(嵌入式数据库,无需安装服务,单文件存储,适合小型 / 中型项目):学基本的增删改查(比如记录每小时产量、故障时间); Excel/CVS:简单的报表导出(比如生产日报表,用 C# 的 NPOI 库); 进阶(可选):MySQL(中小型产线的集中存储),重点学 “定时写入 PLC 数据”(比如每秒写一次实时数据,每分钟汇总一次统计数据)。 实操目标: 把上位机读取的 PLC 产量数据,实时写入 SQLite,能导出成 Excel 日报表。 第二优先级 (解决 “稳定运行” 问题,避免现场出 bug) 1. 多线程 / 异步编程(工业上位机的 “避坑核心”) PLC 是循环扫描机制,而上位机如果用单线程读取 PLC 数据 + 刷新界面,会导致界面卡顿、数据读取超时,必须了解多线程基础: 核心知识点: 主线程(UI 线程):只负责界面刷新,不能做耗时操作(如长时间读取 PLC); 子线程:专门处理 “读写 PLC 数据、数据存储”,避免阻塞 UI; 跨线程更新 UI:C# 的 Invoke(避免直接在子线程改界面控件)。 避坑点: 不要频繁轮询 PLC(比如 1ms 轮询一次),按工业场景设置合理周期(如模拟量 100ms、开关量 300ms),避免通讯过载。 2. 工业场景的异常处理(上位机稳定运行的关键) PLC 工程师懂故障互锁,但上位机需要处理 “通讯中断、数据异常、操作失误” 等场景,重点学: 通讯异常:PLC 断连时的提示(弹窗 / 日志)、重连机制(比如每 5 秒尝试重新连接); 数据异常:PLC 数据超出合理范围(如温度 - 30℃~100℃)时的报警、数据过滤(避免脏数据写入数据库); 操作异常:防止误操作(如连续点击启动按钮多少次,或者弹框提升),添加按钮禁用、权限验证(如管理员才能修改工艺参数)。

    01-04 670浏览
  • ARM基础知识之开发工具

    时间过得真快啊,一直上班累成狗,像一个陀螺一样,被工作和生活推得溜溜转,一晃有段时间没更新文章了!很快中秋节、国庆节了,祝大家度过一个轻松自由的假期。 工欲善其事必先利其器,嵌入式软件开发,基本的工具必须熟练。这里介绍下armcc工具链。 开发流程我们一般是这样开发的: 用c语言或汇编代码编写源码,用vsdode等编辑器,或者Keil集成工具 编译器(armcc,armasm)把源码编译成ELF文件的目标文件 链接器把目标文件连接成ELF格式的可执行映像文件 用formelf工具把可执行的映像文件转换成二进制文件 二进制文件烧录到芯片中,运行 工具简介armcc可以将C语言代码C++语言代码,生成ARM代码或Thumb代码。输出格式为ELF的目标文件,这个目标文件可以包含DWARF格式的调试表。比如:armcc -c -g hello.c -o hello.o-c:只编译,不链接-g:增加编译器源码的调试信息armcc -c -thumb -g hello.c -o hello.o-thumb:目标文件thumb代码 armasmarmasm将汇编代码编译成ARM代码或thumb代码,最终输出格式为ELF的目标文件。也可以决定包含或者不包含DWARF调试表信息。armasm -arm -g hello.s -o hell.o生成带调试信息的ARM代码armasm -thumb -g hello.s -o hell.o生成带调试信息的thumb代码更多信息,查看帮助 目标文件armcc和armasm的输出结果为ELF格式的目标文件。-g用于决定是否生成包含DWARF格式的调试表信息。 armlinkarmlink的工作就是合并一个或多个ELF格式的目标文件的内容,生成一个ELF格式的可执行文件。它主要包含以下工作: 解析目标文件之间的符号引用关系 如果目标文件包含c/c++库函数的调用,c/c++运行时库中提取相应的功能模块 将相应输出段组成相应的输出段 如果目标文件中包含调试信息,删除重复的调试信息 根据用户指定的分组和定位信息,建立映像文件的地址映射关系 重新定位需要从定位的变量的值 生成可执行文件 armlinnk -ro-base 0x00000000 test1.o test2.o -o test.axf将test1.o和test2.o链接链接成一个可执行的映像文件test.axf,其中的-ro-base代表代码段在装载域(load view)和运行域(execution view)里面的起始地址。 formelf通常,可执行映像文件通过调试后,如果需要下载到正式的产品中,需要去除调试信息等。armlink生成的ELF格式的映像文件,通过formelf转换为适合在ROM和RAM中运行的各种格式的二进制文件。一般称呼这种二进制位:Plain binary format 详细参数armcc --arm         生成ARM指令集--thumb     生成Thumb指令集--c90         C语言 (default for .c files)--cpp         C++ 语言(default for .cpp files)-O0           优化等级0,最小优化等级-O1           优化等级1-O2           优化等级2-O3           优化等级3-Ospace     空间优化-Otime       时间优化--cpu处理器型号或CPU型号--cpu list     输出可供选择的处理器列表-o输出文件-c             只编译,不链接--asm       额外生成相应的汇编代码-S             只输出汇编文件,不输出目标代码--interleave   Interleave source with disassembly (use with --asm or -S)-E             预处理c语言-D宏定义-g            生成调试信息-I头文件搜索路径 armasm --list listingfile   让编译器输出它产生的详细的汇编语言列表,并把它写到文件中--depend dependfile    把源文件之间的依赖关系输出到一个文件中,当使用make工具进行编译时,可以使用该文件--errors errorsfile    把标准错误输出到错误文件 -I dir[,dir] 程序搜索领--pd--predefine directive     预定义全局变量--maxcache 最大cache    (default 8MB)--no_esc Ignore C-style (\c) escape sequences--no_warn 关闭警告 -g                        生成调试信息--apcs /指定要使用的过程调用标准--checkreglist Warn about out of order LDM/STM register lists--help 帮助--li 小端模式--bi 大端模式 -M                  把源文件的依赖关系输出到标准输出--MD 把源文件的依赖关系输出到文件inputfile.d--keep 把局部变量放到符号表里--regnames none Do not predefine register names--split_ldm Fault long LDM/STM--unsafe Downgrade certain errors to warnings--via 命令行的长度有限制,在编译的时使用了太多的参数,会超过限制,此时可以把这些参数写到一个文件中,用via来引用--cpu Set the target ARM core type--cpu list                 Output a list of all the selectable CPUs--fpu Set target FP architecture version--fpu list                 Output a list of all selectable FP architectures--thumb Thumb指令集--arm ARM指令集 armlink General options (abbreviations shown capitalised): --help          Print this summary. --output file   Specify the name of the output file. --via file      链接器参数文件Options for specifying memory map information: --partial       对目标文件进行部分链接,输出供以后的目标文件使用,而不是可执行映像文件 --scatter file 分散装载配置文件 --ro-base n     RO输出段的装载域和运行域的起始地址 --rw-base n     RW/ZI输出段的装载域和运行域的起始地址Options for controlling image contents: --bestdebug     包含调试信息 --datacompressor off 不压缩RW输出段数据 --no_debug    映像文件不包含debug信息 --entry         映像文件入口地址 --libpath      系统库文件搜索路径 --userlibpath   用户库文件搜索路径 --no_locals    不包含局部变量 --no_remove     删除没有使用的段Options for controlling image related information: --callgraph     生成函数调用图 --feedback file Generate feedback that can be used by the compiler in file. --info topic    列出映像文件的基本信息List misc. information about image. Available topics: (separate multiple topics with comma) common   List common sections eliminated from the image. debug    List eliminated input debug sections. sizes    List code and data sizes for objects in image. totals   List total sizes of all objects in image. veneers  List veneers that have been generated. unused   List sections eliminated from the image. --map        生成映像文件的信息图 --symbols    列出局部和全局符合及其值 --xref         列出所有输入端的交叉引用 fromelf D:\Program Files\Keil\ARM\ARMCC\bin>fromelfProduct: MDK Plus 5.32Component: ARM Compiler 5.06 update 7 (build 960)Tool: fromelf [4d35f4]For support see http://www.arm.com/supportSoftware supplied by: ARM Limited ARM image conversion utilityfromelf [options] input_file Options: --help         display this help screen --vsn          display version information --output file  the output file. (defaults to stdout for -text format) --nodebug      do not put debug areas in the output image --nolinkview   do not put sections in the output image Binary Output Formats: --bin          Plain Binary --m32          Motorola 32 bit Hex --i32          Intel 32 bit Hex --vhx          Byte Oriented Hex format --base addr    Optionally set base address for m32,i32 Output Formats Requiring Debug Information --fieldoffsets Assembly Language Description of Structures/Classes --expandarrays Arrays inside and outside structures are expanded Other Output Formats: --elf         ELF --text        Text Information Flags for Text Information -v          verbose -a          print data addresses (For images built with debug) -c          disassemble code -d          print contents of data section -e          print exception tables -g          print debug tables -r          print relocation information -s          print symbol table -t          print string table -y          print dynamic segment contents -z          print code and data size information 好的,今天的搬砖工作就到这里,希望可以和你一起进步!

    2025-09-23 1374浏览
  • 大佬带你看嵌入式系统,嵌入式系统该学习什么?

    嵌入式系统是当今的热门系统之一,在诸多领域,嵌入式系统都有所应用。为增进大家对嵌入式系统的认识,小编将为大家介绍嵌入式系统是一个什么样的专业,以及学习嵌入式系统该学习哪些内容。如果你对嵌入式系统具有...

    2025-06-27 394浏览
  • 嵌入式软件开发,用过长度为零的数组没

    众所周知,GNU/GCC在标准的C/C++基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展. 多数情况下, 其应用在变长数组中, 其定义如下: struct Packet { int state; int len; char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持 }; 首先对 0 长度数组, 也叫柔性数组,做一个解释 : 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体; 用法 : 在一个结构体的最后,声明一个长度为 0 的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为 0 的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量 (注意 : 数组名永远都不会是指针!), 但对于这个数组的大小, 我们可以进行动态分配。 注意 :如果结构体是通过calloc、malloc或 者new等动态分配方式生成,在不需要时要释放相应的空间。 优点 :比起在结构体中声明一个指针变量、再进行动态分 配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。 缺点 :在结构体中,数组为 0 的数组必须在最后声明,使用上有一定限制。 对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量! 0 长度数组的用途: 我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个len字段和data字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路: 定长数据缓冲区, 设置一个足够大小MAX_LENGTH的数据缓冲区 设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间 我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟、释放和访问。 1、定长包(开辟空间, 释放, 访问): 比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度MAX_LENGTH为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费: 数据结构定义: //  定长缓冲区 struct max_buffer { int len; char data[MAX_LENGTH]; }; 数据结构大小:考虑对齐, 那么数据结构的大小 >=sizeof(int) + sizeof(char) * MAX_LENGTH 由于考虑到数据的溢出, 变长数据包中的data数组长度一般会设置得足够长足以容纳最大的数据, 因此max_buffer中的 data 数组很多情况下都没有填满数据, 因此造成了浪费。 数据包的构造:假如我们要发送CURR_LENGTH = 1024个字节, 我们如何构造这个数据包呢;一般来说, 我们会返回一个指向缓冲区数据结构max_buffer的指针: //  开辟 if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)     {   mbuffer->len = CURR_LENGTH; memcpy(mbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", mbuffer->len, mbuffer->data); } 访问:这段内存要分两部分使用;前部分 4 个字节p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包,那么这个包到底有多长呢,因此,我们就必须通过一个变量来表明这个数据包的长度,这就是len的作用);而紧接其后的内存是真正的数据部分, 通过p->data, 最后, 进行一个memcpy()内存拷贝, 把要发送的数据填入到这段内存当中 释放:那么当使用完毕释放数据的空间的时候, 直接释放就可以了 // 销毁 free(mbuffer); mbuffer = NULL; 2、小结: 使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间MAX_LENGTH, 而实际使用过程中, 达到MAX_LENGTH长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的 但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作 3、 指针数据包(开辟空间, 释放, 访问): 如果你将上面的长度为MAX_LENGTH的定长数组换为指针, 每次使用时动态的开辟CURR_LENGTH大小的空间, 那么就避免造成MAX_LENGTH - CURR_LENGTH空间的浪费, 只浪费了一个指针域的空间: 数据包定义: struct point_buffer { int len; char *data; }; 数据结构大小:考虑对齐, 那么数据结构的大小 >=sizeof(int) + sizeof(char *) 空间分配:但是也造成了使用在分配内存时,需采用两步 // ===================== // 指针数组  占用-开辟-销毁 // ===================== ///  占用 printf("the length of struct test3:%d\n",sizeof(struct point_buffer)); ///  开辟 if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL) {   pbuffer->len = CURR_LENGTH; if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)   { memcpy(pbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", pbuffer->len, pbuffer->data);   } } 首先, 需为结构体分配一块内存空间;其次再为结构体中的成员变量分配内存空间。 这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它。 释放:相反, 释放时也是一样的: /// 销毁 free(pbuffer->data); free(pbuffer); pbuffer = NULL; 小结: - 使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH 长度的数组, 不会造成空间的大量浪费。 但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向struct point_buffer的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。 4、变长数据缓冲区(开辟空间, 释放, 访问) 定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢? GNU C的 0 长度数组, 也叫变长数组, 柔性数组就是这样一个扩展。对于 0 长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等: 数据结构定义: //  0长度数组 struct zero_buffer { int len; char data[0]; }; 数据结构大小:这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0];只是个数组名, 是不占用存储空间的: sizeof(struct zero_buffer) = sizeof(int) 开辟空间:那么我们使用的时候, 只需要开辟一次空间即可 ///  开辟 if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL) {     zbuffer->len = CURR_LENGTH; memcpy(zbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", zbuffer->len, zbuffer->data); } 释放空间:释放空间也是一样的, 一次释放即可 ///  销毁 free(zbuffer); zbuffer = NULL; 总结: // zero_length_array.c #include #include #define MAX_LENGTH      1024 #define CURR_LENGTH      512 //  0长度数组 struct zero_buffer { int len; char data[0]; }__attribute((packed)); //  定长数组 struct max_buffer { int len; char data[MAX_LENGTH]; }__attribute((packed)); //  指针数组 struct point_buffer { int len; char *data; }__attribute((packed)); int main(void) { struct zero_buffer *zbuffer = NULL; struct max_buffer *mbuffer = NULL; struct point_buffer *pbuffer = NULL; // ===================== // 0长度数组  占用-开辟-销毁 // ===================== ///  占用 printf("the length of struct test1:%d\n",sizeof(struct zero_buffer)); ///  开辟 if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)   {       zbuffer->len = CURR_LENGTH; memcpy(zbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", zbuffer->len, zbuffer->data);   } ///  销毁 free(zbuffer);   zbuffer = NULL; // ===================== // 定长数组  占用-开辟-销毁 // ===================== ///  占用 printf("the length of struct test2:%d\n",sizeof(struct max_buffer)); ///  开辟 if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)   {       mbuffer->len = CURR_LENGTH; memcpy(mbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", mbuffer->len, mbuffer->data);   } /// 销毁 free(mbuffer);   mbuffer = NULL; // ===================== // 指针数组  占用-开辟-销毁 // ===================== ///  占用 printf("the length of struct test3:%d\n",sizeof(struct point_buffer)); ///  开辟 if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)   {       pbuffer->len = CURR_LENGTH; if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)     { memcpy(pbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", pbuffer->len, pbuffer->data);     }   } /// 销毁 free(pbuffer->data); free(pbuffer);   pbuffer = NULL; return EXIT_SUCCESS; } 长度为 0 的数组并不占有内存空间, 而指针方式需要占用内存空间. 对于长度为 0 数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 在申请空间时需分别进行, 释放时也需分别释放. 对于长度为 0 的数组的访问可采用数组方式进行 GNU Document中 变长数组的支持: 参考: 6.17 Arrays of Length Zero C Struct Hack – Structure with variable length array 在C90之前, 并不支持 0 长度的数组, 0 长度数组是GNU C的一个扩展, 因此早期的编译器中是无法通过编译的;对于GNU C增加的扩展,GCC提供了编译选项来明确的标识出他们: -pedantic选项,那么使用了扩展语法的地方将产生相应的警告信息 -Wall使用它能够使GCC产生尽可能多的警告信息 -Werror, 它要求GCC将所有的警告当成错误进行处理 // 1.c #include #include int main(void) { char a[0]; printf("%ld", sizeof(a)); return EXIT_SUCCESS; } 我们来编译: # 显示所有警告 gcc 1.c -Wall #none warning and error # 对GNU C的扩展显示警告 gcc 1.c -Wall -pedantic 1.c: In function ‘main’: 1.c:7: warning: ISO C forbids zero-size array ‘a’ # 显示所有警告同时GNU C的扩展显示警告, 将警告用 error 显示 gcc 1.c -Werror -Wall -pedantic cc1: warnings being treated as errors 1.c: In function ‘main’: 1.c:7: error: ISO C forbids zero-size array ‘a’ 0长度数组其实就是灵活地运用数组指向的是其后面连续的内存空间: struct buffer { int len; char data[0]; }; 在早期没引入 0 长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是: 定长数组定义了一个足够大的缓冲区, 这样使用方便, 但是每次都造成空间的浪费 指针的方式, 要求程序员在释放空间时必须进行多次的free操作, 而我们在使用的过程中往往在函数中返回了指向缓冲区的指针, 我们并不能保证每个人都理解并遵从我们的释放方式。 所以GNU就对其进行了 0 长度数组的扩展. 当使用data[0]的时候, 也就是 0 长度数组的时候,0长度数组作为数组名, 并不占用存储空间。 在C99之后,也加了类似的扩展,只不过用的是char payload[]这种形式(所以如果你在编译的时候确实需要用到-pedantic参数,那么你可以将char payload[0]类型改成char payload[], 这样就可以编译通过了,当然你的编译器必须支持C99标准的,如果太古老的编译器,那可能不支持了) // 2.c payload #include # include  struct payload { int len; char data[]; }; int main(void) { struct payload pay; printf("%ld", sizeof(pay)); return EXIT_SUCCESS; } 使用-pedantic编译后, 不出现警告, 说明这种语法是 C 标准的 gcc 2.c -pedantic -std=c99 所以结构体的末尾, 就是指向了其后面的内存数据。因此我们可以很好的将该类型的结构体作为数据报文的头格式,并且最后一个成员变量,也就刚好是数据内容了. GNU 手册还提供了另外两个结构体来说明,更容易看懂意思: struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } }; struct f2 { struct f1 f1; int data[3]; } f2 = { { 1 }, { 5, 6, 7 } }; 我把 f2 里面的 2,3,4 改成了 5,6,7 以示区分。如果你把数据打出来。即如下的信息: f1.x = 1 f1.y[0] = 2 f1.y[1] = 3 f1.y[2] = 4 也就是f1.y指向的是{2,3,4}这块内存中的数据。所以我们就可以轻易的得到,f2.f1.y指向的数据也就是正好f2.data的内容了。打印出来的数据: f2.f1.x = 1 f2.f1.y[0] = 5 f2.f1.y[1] = 6 f2.f1.y[2] = 7 如果你不是很确认其是否占用空间. 你可以用sizeof来计算一下。就可以知道sizeof(struct f1)=4,也就是int y[]其实是不占用空间的。但是这个 0 长度的数组,必须放在结构体的末尾。如果你没有把它放在末尾的话。编译的时候,会有如下的错误: main.c:37:9: error: flexible array member not at end of struct int y[];                             ^ 到这边,你可能会有疑问,如果将struct f1中的int y[]替换成int *y,又会是如何?这就涉及到数组和指针的问题了. 有时候吧,这两个是一样的,有时候又有区别。 首先要说明的是,支持 0 长度数组的扩展,重点在数组,也就是不能用int *y指针来替换。sizeof的长度就不一样了。把struct f1改成这样: struct f3 { int x; int *y; }; 在 32/64 位下, int 均是 4 个字节,sizeof(struct f1)=4,而sizeof(struct f3)=16 因为int *y是指针, 指针在 64 位下, 是 64 位的,sizeof(struct f3) = 16;如果在32位环境的话,sizeof(struct f3)则是 8 了,sizeof(struct f1)不变. 所以int *y是不能替代int y[]的; 代码如下: // 3.c #include #include struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } }; struct f2 { struct f1 f1; int data[3]; } f2 = { { 1 }, { 5, 6, 7 } }; struct f3 { int x; int *y; }; int main(void) { printf("sizeof(f1) = %d\n", sizeof(struct f1)); printf("sizeof(f2) = %d\n", sizeof(struct f2)); printf("szieof(f3) = %d\n\n", sizeof(struct f3)); printf("f1.x = %d\n", f1.x); printf("f1.y[0] = %d\n", f1.y[0]); printf("f1.y[1] = %d\n", f1.y[1]); printf("f1.y[2] = %d\n", f1.y[2]); printf("f2.f1.x = %d\n", f1.x); printf("f2.f1.y[0] = %d\n", f2.f1.y[0]); printf("f2.f1.y[1] = %d\n", f2.f1.y[1]); printf("f2.f1.y[2] = %d\n", f2.f1.y[2]); return EXIT_SUCCESS; } 0 长度数组的其他特征: 1、为什么 0 长度数组不占用存储空间: 0 长度数组与指针实现有什么区别呢, 为什么0长度数组不占用存储空间呢? 其实本质上涉及到的是一个 C 语言里面的数组和指针的区别问题。char a[1]里面的a和char *b的b相同吗? 《 Programming Abstractions in C》(Roberts, E. S.,机械工业出版社,2004.6)82页里面说: “arr is defined to be identical to &arr[0]”. 也就是说,char a[1]里面的a实际是一个常量,等于&a[0]。而char *b是有一个实实在在的指针变量b存在。所以,a=b是不允许的,而b=a是允许的。两种变量都支持下标式的访问,那么对于a[0]和b[0]本质上是否有区别?我们可以通过一个例子来说明。 参见如下两个程序gdb_zero_length_array.c和gdb_zero_length_array.c: //  gdb_zero_length_array.c #include #include struct str { int len; char s[0]; }; struct foo { struct str *a; }; int main(void) { struct foo f = { NULL }; printf("sizeof(struct str) = %d\n", sizeof(struct str)); printf("before f.a->s.\n"); if(f.a->s)   { printf("before printf f.a->s.\n"); printf(f.a->s); printf("before printf f.a->s.\n");   } return EXIT_SUCCESS; } \ //  gdb_pzero_length_array.c #include # include  struct str { int len; char *s; }; struct foo { struct str *a; }; int main(void) { struct foo f = { NULL }; printf("sizeof(struct str) = %d\n", sizeof(struct str)); printf("before f.a->s.\n"); if (f.a->s)   { printf("before printf f.a->s.\n"); printf(f.a->s); printf("before printf f.a->s.\n");   } return EXIT_SUCCESS; } 可以看到这两个程序虽然都存在访问异常, 但是段错误的位置却不同 我们将两个程序编译成汇编, 然后diff查看他们的汇编代码有何不同 gcc -S gdb_zero_length_array.c -o gdb_test.s gcc -S gdb_pzero_length_array.c -o gdb_ptest diff gdb_test.s gdb_ptest.s 1c1 < .file "gdb_zero_length_array.c" --- >   .file "gdb_pzero_length_array.c" 23c23 < movl $4, %esi --- >   movl    $16, %esi 30c30 < addq $4, %rax --- >   movq 8(%rax), %rax 36c36 < addq $4, %rax --- >   movq 8(%rax), %rax # printf("sizeof(struct str) = %d\n", sizeof(struct str)); 23c23 < movl $4, %esi #printf("sizeof(struct str) = %d\n", sizeof(struct str)); --- >   movl    $16, %esi #printf("sizeof(struct str) = %d\n", sizeof(struct str)); 从 64 位系统中, 汇编我们看出, 变长数组结构的大小为 4, 而指针形式的结构大小为 16: f.a->s 30c30/36c36 < addq $4, %rax --- >   movq 8(%rax), %rax 可以看到有: 对于char s[0]来说, 汇编代码用了addq指令,addq $4, %rax 对于char *s来说,汇编代码用了movq指令,movq 8(%rax), %rax addq对%rax + sizeof(struct str), 即str结构的末尾即是char s[0]的地址, 这一步只是拿到了其地址, 而movq则是把地址里的内容放进去, 因此有时也被翻译为leap指令, 参见下一例子 从这里可以看到, 访问成员数组名其实得到的是数组的相对地址, 而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的): 访问相对地址,程序不会crash,但是,访问一个非法的地址中的内容,程序就会crash。 // 4-1.c #include #include int main(void) { char *a; printf("%p\n", a); return EXIT_SUCCESS; } 4-2.c #include #include int main(void) { char a[0]; printf("%p\n", a); return EXIT_SUCCESS; } $ diff 4-1.s 4-2.s 1c1 < .file "4-1.c" --- >       .file "4-2.c" 13c13 < subl $16, %esp --- >       subl    $32, %esp 15c15 < leal 16(%esp), %eax --- >       movl 28(%esp), %eax 对于char a[0]来说, 汇编代码用了leal指令,leal 16(%esp), %eax: 对于char *a来说,汇编代码用了movl指令,movl 28(%esp), %eax 2、地址优化: // 5-1.c #include #include int main(void) { char a[0]; printf("%p\n", a); char b[0]; printf("%p\n", b); return EXIT_SUCCESS; } img 由于 0 长度数组是 GNU C 的扩展, 不被标准库任可, 那么一些巧妙编写的诡异代码, 其执行结果就是依赖于编译器和优化策略的实现的. 比如上面的代码, a 和 b 的地址就会被编译器优化到一处, 因为a[0]和b[0]对于程序来说是无法使用的, 这让我们想到了什么? 编译器对于相同字符串常量, 往往地址也是优化到一处, 减少空间占用: //  5-2.c #include #include int main(void) { const char *a = "Hello"; printf("%p\n", a); const char *b = "Hello"; printf("%p\n", b); const char c[] = "Hello"; printf("%p\n", c); return EXIT_SUCCESS; }

    2025-05-14 375浏览
  • 提升嵌入式开发能力,一定要注意这几点

    有一些人虽然工作了很多年,但工作表现就像刚入行的新人。他们几乎不学习软件开发的基础知识 。除了最初几年有所成长,后期一直停滞不前,而且他们不明白为什么。 与此同时,我也曾与一些只有几年工作经验的开发人员共事,他们表现出惊人的增长潜力。他们工作态度端正,并且明白如何避免不称职的行为。 根据开发人员的某些习惯,可以非常明显地分辨出谁更专业,谁更业余。让我们深入剖析下业余程序开发人员的几种表现,每个程序开发人员都应该引以为戒,这些错误会阻碍我们的职业发展。 一次性提交大量代码 回忆下,你是否碰到过一次性提交大量代码的人,你都不想给他做代码评审。是的,不专业的开发人员就会这样做。他们会在一次代码评审请求中包含多个模块的修改,而且会催促你优先评审他们的代码。 是啊,能不急吗,排到后边,还需要解决代码冲突的问题。这个问题在很多高级开发工程师中也存在,他们在功能开发期间不做任何提交,只有在功能彻底完工后,才会提交所有修改,于是代码评审中的任何意见都会引起大量的修改。 当我碰到这种代码评审请求时,我首先做的是要求提交者按功能模块将其拆分成多个小的请求。 我只会对 issues(任务管理系统)中的第一个功能需求评审,然后将其转回提交者。如果我有时间,我会和提交者连线进行代码实时评审。 你能做什么? 进行小的代码提交。一个好的做法是:每个工作日都进行代码提交。 不要提交没有编译或者会导致构建失败的代码。 代码写的很烂 缺乏经验的开发人员写不出漂亮的代码,他们写出的代码会很混乱,而且分布在代码库的各个部分。 当你尝试阅读这类代码时,会感觉自己身处一座迷宫之中。你会逐渐忘记自己是从什么地方开始的,要寻找什么以及这段代码完成了什么功能。 有经验的开发人员知道代码如何设计。除非要开发的功能显而易见,首先需要在纸上写出你对需求的理解并画出流程图(简化版的规格需求说明书),在脑海里对这段代码进行一个完整的构思。除非你彻底弄清楚了如何修改,否则不要开始代码编写。 如果你不遵守以上的规则,当你回顾自己完成的代码时会非常痛苦。以后如果需要修正问题或者增加功能,也会变得非常棘手。 你能做什么? 编写代码之前,对你要实现的功能有个清晰的了解。为了清楚地理解需求,你需要尽量多问问题。 让你的代码简洁而优雅。其他团队成员可以读懂代码并理解它打算做什么。 同时开展多项工作 缺乏经验的开发人员不知道什么时候开始一项任务、如何推进、什么时候结束。他们试图并行处理多项任务。他们不知道如何将一项大任务分解为小的模块,从而减轻实现的难度。 当他们收到一项任务时,并不是第一时间和上级确认需求,而是立刻就开始编程,而且在做任务期间,也不会和上级就任务进度进行沟通。 只有当任务完成时,他们才会向你反馈。到那个时候,你只能祈祷他们完成的功能就是你想要的。 缺乏经验的开发人员的另一个表现是同时推进多项任务,他们会同时处理多项事情,如:实现多个没有太大联系的功能点、解决生产环境问题、协助其他同事工作等。 最终,从他们那里得不到有效的产出。虽然他们的态度和出发点是好的,但对整个团队造成的后果是灾难性的,浪费了很多的时间,导致团队得日夜赶工。 你能做什么? 专注完成小的任务。将收到的任务分解为小块,明确需求的优先级,一小块一小块地完成。 领取一项任务,完成后再开始新的任务。 性格傲慢 对于缺乏经验的开发人员,傲慢是非常致命的。傲慢会导致他们不能接受别人的批评和建议。当你对他们的代码或者陈述给出意见时,他们会认为你是在质疑他们的能力。 许多新人由于无知,都会表现出这种傲慢。刚走出校门的他们充满自信,并没有意识到他们在学校学到的东西离社会要求还有很大差距。这些人中的聪明者会很快调整自己,以归零的心态,努力学习并适应公司文化。 其实不只是新人——一些有几年工作经验的开发人员也会表现出这种傲慢,一部分原因是其满足于个人获得的专业成就,另一部分可能的原因是其缺乏和优秀的人共事的机会,有点坐井观天。 此外,傲慢的行为也从另一方面证明这样的开发人员确实缺乏经验。这样的行为会对他们的职业发展造成很多阻碍,因为没有人喜欢和一个傲慢的人共事。当成长变慢时,他们不会从自身找原因,而是更多的归罪于别人。 你能做什么 在前行的路上保持谦卑。礼貌地对待别人会让你在软件开发职业生涯中走得更远。 尊重每一个人。出现分歧后,在你发表意见时,不管对方是什么身份,都要尊重对方。 不能从之前的错误中学到经验 我一直认为,对于软件开发人员,反馈机制是一个很有效的工具。来自他人的反馈,会让我们明白自己的短板是什么以及如何去改进。一个聪明的开发人员明白如何借助他人反馈来促进自己的成长。 根据一个开发人员对建设性意见的反应,你可以判断出他是否缺乏经验。缺乏经验的开发人员不接受任何建设性的建议,甚至代码评审中的评论,他都会认为是对他个人的一种攻击。 很多年前,我有一个同事给我写了很长的一封邮件,教我如何来评审代码,他对我给他代码的评论感到愤怒。他的主要观点是我不应该关注编码标准,因为他知道如何编码,我应该只关注代码能否满足功能需求。 如果一个开发人员因为别人对他代码给出的评论,而感觉被冒犯,只能表明他不具有真正的开发经验。他抱着做一天和尚撞一天钟的态度工作,却感慨没有遇到赏识自己的伯乐。 你能做什么? 对每个反馈保持积极的态度。对于每个反馈,你可以选择是接受还是拒绝,但拒绝之前要保持心平气和的态度。 从错误中学习。没有人能永远正确,保持终身学习才能让自己持续强大。 工作时间处理私人事务 日常工作中,总是发现团队里的一些成员在工作时间处理私人事务,如:看社交媒体,浏览购物网站,玩游戏。 我之前还有个团队成员,上班时间炒股。因为他需要不时地关注股票的 K 线走势,造成个人的产出质量不高。其他同事对他很有意见,因为他们需要花费更多的时间去赶工期。 当开发经理和这个开发人员谈话之后,他改变了一段时间,但是很快就故态复萌。最终,公司只能把他开除了。 工作时间处理私人事务,这是违反商业道德,并且表现了你的不专业。我们需要对工作敬业,毕竟我们要靠它谋生。 你能做什么? 工作时间尽量不要处理私人事务。当你需要离开几个小时去处理个人事情时,请向你的管理者请假。 使用休息时间浏览你的社交媒体。如果必须要点外卖或炒股,请利用午休时间。 盲目追逐技术潮流 开发人员缺乏经验的另一个表现是面对技术潮流的态度。你会发现他们总是在谈论技术潮流,当有一个新的潮流出现时,他们会立刻丢弃原来的潮流,投入新的怀抱。 缺乏经验的开发人员总是在学习教程。毫无疑问,教程是很有用的学习工具,但是,不进行任何实践而只是按照教程一步步操作无疑是浪费时间。 它会让你虚幻地觉得自己好像都掌握了,但是知识是否掌握了,需要通过真实的项目进行检验。 开发人员很少会用热门技术或者从教程中学到的知识来实现新的东西,他们学习热门技术或者教程很多是为了满足自己的虚荣心,或者担心自己会错过什么。 你能做什么? 花费时间和精力学习那些能在工作中或者实际项目中真正用到的技术。 从教程中学习并及时练习,相对于新手教程,自己实现一个功能能学到更多的东西。 缺乏经验的开发人员会因为自己的效率低下进而降低整个团队的效率。他对待自己工作的错误态度,会让其在职业发展中错失很多机会。 了解并避免这种错误的态度和工作方式,是聪明人的做法。如果你不幸染上了这些坏习惯,随着时间的推移,你会越来越难以摆脱。

    2025-05-09 325浏览
  • 深度解析C++异常处理机制:最佳实践、性能分析和挑战

    一、异常处理实践在编写 C++ 代码时会遇到不可预期的错误和异常情况。为了让我们的代码更健壮和可靠,我们需要使用异常处理机制来处理这些情况。

    2025-04-18 468浏览
  • 常用的嵌入式软件调试方法与技巧

    在软件开发过程中,一般来说,花在测试比花在编码的时间要多很多,通常为3:1(甚至更多)。 这个比例随着你的编程和测试水平的提高而不断下降,但不论怎样,软件测试对一般人来讲很重要。 今天以嵌入式开发为例,给大家分享一下常见软件的调试方法有哪些? 很多年前,一位开发人员为了在对嵌入式有更深层次的理解,询问了这样的一个问题:我怎么才能知道并懂得我的系统到底在干些什么呢? 面对这个问题有些吃惊,因为在当时没有人这么问过,而同时代的嵌入式开发人员问的最多的大都围绕“我怎么才能使程序跑得更快”、“什么编译器最好”等问题。 面对这个不同寻常却异乎成熟的问题,可能很多人都不知道怎么办,下面就来讲讲软件测试找bug常见方法和秘诀。 1 懂得使用工具 通常嵌入式系统对可靠性的要求比较高。嵌入式系统安全性的失效可能会导致灾难性的后果,即使是非安全性系统,由于大批量生产也会导致严重的经济损失。 这就要求对嵌入式系统,包括嵌入式软件进行严格的测试、确认和验证。随着越来越多的领域使用软件和微处理器控制各种嵌入式设备,对日益复杂的嵌入式软件进行快速有效的测试愈加显得重要。 就像修车需要工具一样,好的程序员应该能够熟练运用各种软件工具。 不同的工具,有不同的使用范围,有不同的功能。使用这些工具,你可以看到你的系统在干些什么,它又占用什么资源,它到底和哪些外界的东西打交道。 让你郁闷好几天的问题可能通过某个工具就能轻松搞定,可惜你就是不知道。 那么为什么那么多的人总是在折腾个半死之后才想到要用测试工具呢?原因很多,主要有两个: 一个是害怕; 另一个是惰性; 害怕是因为加入测试工具或测试模块到代码需要技巧同时有可能引入新的错误,所以他们总喜欢寄希望于通过不断地修改重编译代码来消除bug,结果却无济于事。 懒惰是因为他们习惯了使用printf之类的简单测试手段。 下面来介绍一些嵌入式常用的测试工具(1)、源码级调试器????[Source-levelDebugger] 这种调试器一般提供单步或多步调试、断点设置、内存检测、变量查看等功能,是嵌入式调试最根本有效的调试方法。比如VxWorksTornadoII提供的gdb就属于这一种。 (2)、简单实用的打印显示工具???? [printf] printf或其它类似的打印显示工具估计是最灵活最简单的调试工具。 打印代码执行过程中的各种变量可以让你知道代码执行的情况。但是,printf对正常的代码执行干扰比较大(一般printf占用CPU比较长的时间),需要慎重使用,最好设置打印开关来控制打印。 (3)、ICE或JTAG调试器????[In- circuitEmulator] ICE是用来仿真CPU核心的设备,它可以在不干扰运算器的正常运行情况下,实时的检测CPU的内部工作情况。 像桌面调试软件所提供的:复杂的条件断点、先进的实时跟踪、性能分析和端口分析这些功能,它也都能提供。ICE一般都有一个比较特殊的CPU,称为外合(bond-out)CPU. 这是一种被打开了封装的CPU,并且通过特殊的连接,可以访问到CPU的内部信号,而这些信号,在CPU被封装时,是没法 “看到”的。 当和工作站上强大的调试软件联合使用时,ICE就能提供你所能找到的最全面的调试功能。 但ICE同样有一些缺点:昂贵;不能全速工作;同样,并不是所有的CPU都可以作为外合CPU的,从另一个角度说,这些外合CPU也不大可能及时的被新出的CPU所更换。 JTAG(JointTestActionGroup)虽然它最初开发出来是为了监测IC和电路连接,但是这种串行接口扩展了用途,包括对调试的支持。 (4)、ROM监视器???? [ROMMonitor] ROM监控器是一小程序,驻留在嵌入系统ROM中,通过串行的或网络的连接和运行在工作站上的调试软件通信。 这是一种便宜的方式,当然也是最低端的技术。它除了要求一个通信端口和少量的内存空间外,不需要其它任何专门的硬件。 提供了如下功能:下载代码、运行控制、断点、单步步进、以及观察、修改寄存器和内存。 因为ROM监控器是操作软件的一部分,只有当你的应用程序运行时,它才会工作。 如果你想检查CPU和应用程序的状态,你就必须停下应用程序,再次进入ROM监控器。 (5)、Data监视器???? [DataMonitor] 这种监视器在不停止CPU运行的情况下不仅可以显示指定变量内容,还可以收集并以图形形式显示各个变量的变化过程。 (6)、OS监视器???? [OperatingSystemMonitor] 操作系统监视器可以显示诸如任务切换、信号量收发、中断等事件。 一方面,这些监视器能够为你呈现事件之间的关系和时间联系;另一方面,还可以提供对信号量优先级反转、死锁和中断延时等问题的诊断。 (7)、性能分析工具???? [Profiler] 可以用来测试CPU到底耗在哪里。profiler工具可以让你知道系统的瓶颈在哪里、CPU的使用率以及需要优化的地方。 (8)、内存测试工具???? [MemoryTeseter] 可以找到内存使用的问题所在,比如内存泄露、内存碎片、内存崩溃等问题。如果发现系统出现一些不可预知的或间歇性的问题,就应该使用内存测试工具测测看。 (8)、运行跟踪器???? [ExecutionTracer] 可以显示CPU执行了哪些函数、谁在调用、参数是什么、何时调用等情况。这种工具主要用于测试代码逻辑,可以在大量的事件中发现异常。 (9)、覆盖工具[CoverageTester] 主要显示CPU具体执行了哪些代码,并让你知道那些代码分支没有被执行到哪里。这样有助于提高代码质量并消除无用代码。 (10)、GUI测试工具???? [GUITester] 很多嵌入式应用带有某种形式的图形用户界面进行交互,有些系统性能测试是根据用户输入响应时间进行的。 GUI测试工具可以作为脚本工具有开发环境中运行测试用例,其功能包括对操作的记录和回放、抓取屏幕显示供以后分析和比较、设置和管理测试过程(Rational 公司的robot和Mercury的Loadrunner工具是杰出的代表)。 很多嵌入式设备没有GUI,但常常可以对嵌入式设备进行插装来运行GUI测试脚本,虽然这种方式可能要求对被测代码进行更改,但是节省了功能测试和回归测试的时间。 (11)、自制工具???? [Home-madetester] 在嵌入式应用中,有时候为了特定的目的,需要自行编写一些工具来达到某种测试目的。 本人曾经编写的视频流录显工具在测试视频会议数据流向和变化上帮了大忙,帮公司找到了几个隐藏很深的bug。 2 尽早发现内存问题 内存问题危害很大,不容易排查,主要有三种类型:内存泄露、内存碎片和内存崩溃。 对于内存问题态度必须要明确,那就是早发现早“治疗”。在软件设计中,内存泄露的“名气”最大,主要由于不断分配的内存无法及时地被释放,久而久之,系统的内存耗尽。 即使细心的编程老手有时后也会遭遇内存泄露问题。有测试过内存泄露的朋友估计都有深刻地体验,那就是内存泄露问题一般隐藏很深,很难通过代码阅读来发现。有些内存泄露甚至可能出现在库当中。 有可能这本身是库中的bug,也有可能是因为程序员没有正确理解它们的接口说明文档造成错用。 在很多时候,大多数的内存泄露问题无法探测,但可能表现为随机的故障。程序员们往往会把这种现象怪罪于硬件问题。 如果用户对系统稳定性不是很高,那么重启系统问题也不大;但,如果用户对系统稳定很高,那么这种故障就有可能使用户对产品失去信心,同时也意味着你的项目是个失败的项目。 由于内存泄露危害巨大,现在已经有许多工具来解决这个问题。 这些工具通过查找没有引用或重复使用的代码块、垃圾内存收集、库跟踪等技术来发现内存泄露的问题。 每个工具都有利有弊,不过总的来说,用要比不用好。总之,负责的开发人员应该去测试内存泄露的问题,做到防患于未然。 内存碎片比内存泄露隐藏还要深。随着内存的不断分配并释放,大块内存不断分解为小块内存,从而形成碎片,久而久之,当需要申请大块内存是,有可能就会失败。如果系统内存够大,那么坚持的时间会长一些,但最终还是逃不出分配失败的厄运。在使用动态分配的系统中,内存碎片经常发生。 目前,解决这个问题最效的方法就是使用工具通过显示系统中内存的使用情况来发现谁是导致内存碎片的罪魁祸首,然后改进相应的部分。 由于动态内存管理的种种问题,在嵌入式应用中,很多公司干脆就禁用malloc/free的以绝后患。 内存崩溃是内存使用最严重的结果,主要原因有数组访问越界、写已经释放的内存、指针计算错误、访问堆栈地址越界等等。 这种内存崩溃造成系统故障是随机的,而且很难查找,目前提供用于排查的工具也很少。 总之,如果要使用内存管理单元的话,必须要小心,并严格遵守它们的使用规则,比如谁分配谁释放。 3 深入理解代码优化 讲到系统稳定性,人们更多地会想到实时性和速度,因为代码效率对嵌入式系统来说太重要了。 知道怎么优化代码是每个嵌入式软件开发人员必须具备的技能。就像女孩子减肥一样,起码知道她哪个地方最需要减,才能去购买减肥药或器材来减掉它。 可见,代码优化的前提是找到真正需要优化的地方,然后对症下药,优化相应部分的代码。 前面提到的profile(性能分析工具,一些功能齐全IDE都提供这种内置的工具)能够记录各种情况比如各个任务的CPU占用率、各个任务的优先级是否分配妥当、某个数据被拷贝了多少次、访问磁盘多少次、是否调用了网络收发的程序、测试代码是否已经关闭等等。 但是,profile工具在分析实时系统性能方面还是有不够的地方。 一方面,人们使用profile工具往往是在系统出现问题即CPU耗尽之后,而 profile工具本身对CPU占用较大,所以profile对这种情况很可能不起作用。 根据Heisenberg效应,任何测试手段或多或少都会改变系统运行,这个对profiler同样适用! 总之,提高运行效率的前提是你必须要知道CPU到底干了些什么干的怎么样。 4 不要让自己大海捞针 大海捞针只是对调试的一种生动比喻。经常听到组里有人对自己正在调试的代码说shit! 可以理解,因为代码不是他写的,他有足够的理由去 shitbug百出的代码,只要他自己不要写出这种代码,否则有一天同组的其它人可能同样会shit他写的代码。 为何会有大海捞针呢?肯定是有人把针掉到海里咯;那针为何会掉在海里呢?肯定是有人不小心或草率呗。 所以当你在抱怨针那么难找的时候,你是否想过是你自己草率地丢掉的。 同样,当你调试个半死的时候,你是否想过你要好好反省一下当初为了寻求捷径可能没有严格地遵守好的编码设计规范、没有检测一些假设条件或算法的正确性、没有将一些可能存在问题的代码打上记号呢? 关于如何写高质量请参考林锐的《高质量c++/c编程指南》或《关于C的0x8本“经书》。 如果你确实已经把针掉在海里是,为了防止在找到之前刺到自己,你必须要做一些防范工作,比如戴上安全手套。 同样,为了尽能地暴露和捕捉问题根源,我们可以设计比较全面的错误跟踪代码。怎么来做呢? 尽可能对每个函数调用失败作出处理,尽可能检测每个参数输入输出的有效性,包括指针以及检测是否过多或过少地调用某个过程。错误跟踪能够让你知道你大概把针掉在哪个位置。 5 重现并隔离问题 如果你不是把针掉在大海了,而是掉在草堆里,那要好办些。因为至少我们可以把草堆分成很多块,一块一块的找。 对于模块独立的大型项目,使用隔离方法往往是对付那些隐藏极深bug的最后方法。 如果问题的出现是间歇性的,我们有必要设法去重现它并记录使其重现的整个过程以备在下一次可以利用这些条件去重现问题。 如果你确信可以使用记录的那些条件去重现问题,那么我们就可以着手去隔离问题。怎么隔离呢? 我们可以用#ifdef把一些可能和问题无关的代码关闭,把系统最小化到仍能够重现问题的地步。 如果还是无法定位问题所在,那么有必要打开“工具箱”了。可以试着用ICE或数据监视器去查看某个可疑变量的变化;可以使用跟踪工具获得函数调用的情况包括参数的传递;检查内存是否崩溃以及堆栈溢出的问题。 6 以退为进 猎人为了不使自己在森林里迷路,他常常会在树木上流下一些标记,以备自己将来有一天迷路时可以根据这些标记找到出路。对过去代码的修改进行跟踪记录对将来出现问题之后的调试很有帮助。 假如有一天,你最近一次修改的程序跑了很久之后忽然死掉了,那么你这时的第一反映就是我到底改动了些什么呢,因为上次修改之前是好的。 那么如何检测这次相对于上次的修改呢?没错,代码控制系统SCS或称版本控制系统 VCS可以很好地解决这个问题。 将上个版本checkin下来后和当前测试版本比较。比较的工 具可以是SCS/VCS/CVS自带的diff工具或其它功能更强的比较工具,比如BeyondCompare和 ExamDiff。通过比较,记录所有改动的代码,分析所有可能导致问题的可疑代码。 7 确定测试的完整性 你怎么知道你的测试有多全面呢?覆盖测试(coveragetesting)可以回答这个问题。覆盖测试工具可以告诉你CPU到底执行了哪些代码。 好的覆盖工具通常可以告诉你大概20%到40% 代码没有问题,而其余的可能存在bug.覆盖工具有不同的测试级别,用户可以根据自己的需要选择某个级别。 即使你很确信你的单元测试已经很全面并且没有 deadcode,覆盖工具还是可以为你指出一些潜在的问题。 看下面的代码: if(i>=0&& (almostAlwaysZero==0||(last=i))) 如果almostAlwaysZero为非0,那么last=i赋值语句就被跳过,这可能不是你所期望的。 这种问题通过覆盖工具的条件测试功能可以轻松得被发现。总之,覆盖测试对于提高代码质量很有帮助。 8 提高代码质量意味着节省时间 有研究表明软件开发的时间超过80%被用在下面几个方面:调试自己的代码(单元测试)。 调试自己和其他相关的代码(模块间测试)。调试整个系统(系统测试),更糟糕的是你可能需要花费10-200倍的时间来找一个 bug,而这个bug在开始的时候可能很容易就能找到。 一个小bug可能让你付出巨大的代价,即使这个bug对整个系统的性能没有太大的影响,但很可能会影响让那些你可以看得到的部分。 所以我们必须要养成良好的编码和测试手段以求更高的代码质量,以便缩短调试的代码。 9 发现它、分析它、解决它 这世界没有万能的膏药。profile再强大也有力不从心的时候;内存监视器再好,也有无法发现的时候;覆盖工具再好用,也有不能覆盖的地方。 一些隐藏很深的问题即使用尽所有工具也有可能无法查到其根源,这时我们能做的就是通过这些问题所表现出来的外在现象或一些数据输出来发现其中的规律或异常。 一旦发现任何异常,一定要深入地理解并回溯其根源,直到解决为止。 10 请利用初学者思维 有人这样说过:“有些事情在初学者的脑子里可能有各种各样的情况,可在专家的头脑里可能就很单一”。 有时候,有些简单的问题会被想得很复杂,有些简单的系统被设计得很复杂,就是由于你的“专家思维”。 当你被问题难住时,关掉电脑,出去走走,把你的问题和你的朋友甚至你的小狗说说,或许他们可以给你意想不到的启发。 11 总结 嵌入式调试也是一门艺术。就想其它的艺术一样,如果你想取得成功,你必须具备智慧、经验并懂得使用工具。

    2025-01-02 610浏览
  • 硬件工程师也需要牢记的10大软件技巧

    嵌入式系统设计不仅需要了解硬件,还需了解软件是如何影响硬件并与硬件进行交互的。设计硬件所需的范式可能与设计软

    2024-09-13 576浏览
正在努力加载更多...
广告