CH55xDuino编译报错sdcc.sh语法错误?CRLF换行符才是真凶
第一次用CH55xDuino在Arduino IDE里编译CH552程序时我遇到了一模一样的红色报错sdcc.sh: syntax error: unexpected (。当时第一反应是SDCC装坏了第二反应是板卡包和IDE版本不兼容结果重装了三次SDCC、换了两个CH55xDuino版本都没用。最后静下心来跟了一遍完整编译链才发现这根本不是SDCC的问题而是sdcc.sh这个Shell脚本在从Windows生态迁移到类Unix生态时被一行看不见的回车符给卡住了。这篇文章把我当时的完整排查思路、修复手段和后续预防经验都写下来给碰到相同报错的朋友一条捷径。1. 先把SDCC.sh在编译链里的位置搞明白1.1 CH55xDuino的编译流水线CH55xDuino的项目性质是把沁恒的CH55x系列USB单片机CH551、CH552、CH554、CH559这几个型号最常见塞进Arduino生态里。你可以在Arduino IDE里用标准Arduino语法写digitalWrite、Serial.print点一下编译就生成烧录文件。但CH55x的内核不是AVR是增强型8051E8051Arduino官方那套基于AVR-GCC的工具链根本没法编译它。CH55xDuino选择的是SDCCSmall Device C Compiler开源8051 C编译器这是项目最底层的编译基石。板卡包装好之后核心目录结构大致长这样hardware/arduino/CH55xDuino/ ├── boards.txt ├── platform.txt ├── cores/ ├── libraries/ └── tools/这里面的platform.txt是整个编译流程的剧本。Arduino IDE每做一步操作——预处理、编译、汇编、链接、生成hex——都会去platform.txt里找对应的recipe然后执行recipe里写好的命令行。CH55xDuino在tools/sdcc/bin/下放了一个sdcc.sh脚本Windows对应版本是sdcc.batplatform.txt里的编译命令会调用这个脚本再由脚本去调用真正的SDCC可执行文件。为什么要这么绕一层因为CH55xDuino要同时支持Windows、macOS、Linux三套环境直接写死SDCC的绝对路径肯定不行包作者就用一个脚本做统一入口顺带处理一些参数适配的脏活。1.2 sdcc.sh到底帮SDCC做了什么我手头拿到一个出问题的sdcc.sh时习惯先把它打开看一眼了解它到底在干什么。一个典型的sdcc.sh不会太复杂核心逻辑基本是下面这个路子#!/bin/bash # 简化示例展示sdcc.sh承担的工作 SDCC_BIN$(dirname $0)/sdcc ARGS() for arg in $; do case $arg in -Wl*) ARGS(${arg#-Wl}) ;; *) ARGS($arg) ;; esac done exec $SDCC_BIN ${ARGS[]}这个脚本干了三件事定位同目录下的SDCC可执行文件、把Arduino IDE传过来的AVR风格参数做等价转换、然后exec替换成真正的SDCC进程。有的版本还会根据BOARD_TAG追加预处理宏比如-DCH552之类。关键在于脚本里每一条语句都要求能被当前Shell正确解析。而Arduino IDE在macOS和Linux下调用脚本时用的默认Shell往往不是你想象的bash可能是/bin/sh在某些发行版里/bin/sh又软链接到了dash。这个细节后面会详细说但你先记住脚本的语法兼容性、文件本身的文本格式任何一个出问题都会在编译最开始阶段爆炸。还有一点值得注意sdcc.sh报错有个很明显的特征——它不是编译到一半崩掉而是脚本刚开始解析就报错。也就是说SDCC本身根本没被真正执行到程序还没起跑门卫就已经把路给拦了。很多人围绕SDCC版本、代码语法排查半天方向其实错了。2. unexpected (是bash解析层报错根本原因往往在行尾符2.1 用最小脚本复现同一个语法错误错误信息sdcc.sh: syntax error: unexpected (里那个(很容易让人联想到代码里的括号配对问题。但bash的语法错误提示并不精确到这个程度它只是在某个词法token后面发现了一个不该出现的左括号就干脆终止解析了。我在本地复现过一次。写一个最简单的、语法完全合法的脚本#!/bin/bash function main() { echo hello } main正常情况下执行bash test.sh输出hello。但如果我用sed -i s/$/\r/ test.sh给每一行强行加上Windows回车符再执行一次就得到了经典报错test.sh: syntax error: unexpected (。注意报错里的措辞就和你看到的sdcc.sh一模一样。这已经能说明问题不是SDCC有问题不是代码有问题是脚本文件本身在词法层就没被bash认下来。2.2 CRLF如何骗过bash的词法分析Linux和macOS下的换行符标准是LFLine Feed\nWindows下的文本文件则是CRLFCarriage Return Line Feed\r\n。当你在Windows上用记事本或某些默认编辑器改过Shell脚本再把它放到Linux环境运行时文件末尾就会多出一个隐藏的\r字符。问题就出在这个\r上。bash的词法分析器读脚本时看到的是这样一串内容function main() \r{正常情况下function是bash的保留字后面跟着main()是合法的函数定义语法。可一旦前面多了一个\rbash看到的实际上是\rfunction。这个带回车符的单词不是保留字更不是合法函数名bash在它后面看到一个(自然就把它判定为unexpected。简单说回车符改变了token边界把一条本来合法的语句变成了垃圾语法。如果你觉得不可能这么巧我可以再补一个常见出错的现场用户在Windows上把CH55xDuino的release包解压出来又用记事本打开sdcc.sh改了一个路径保存然后整个目录拷到Linux板子或云主机上编译。文件里每一行末尾都带着\rbash读一行崩一行。这类问题在Git协作里也特别常见——Windows上git checkout时的core.autocrlftrue设置会顺手把所有文本文件从LF改成CRLF。2.3 sh和bash对函数定义语法的容忍度差异CRLF只是最常见的原因还有一种情况也会报出几乎一模一样的错误系统里/bin/sh指向的是dash而脚本用了bash专属语法。dash是Debian系发行版为了开机加速而使用的POSIX兼容Shell它比bash精简得多对function关键字、[[ ]]条件测试、source命令这些bash特性只给一个态度——不认。有人会问脚本开头不是写了#!/bin/bash吗可Arduino IDE调脚本的方式不一定会遵守这个shebang。如果platform.txt里写的是sh sdcc.shShell会忽略文件开头那行注释直接用/bin/sh去解析脚本内容。在dash环境下哪怕脚本是干净的LF换行只要它写了function main() {dash也会报syntax error: unexpected (。所以排查的第一步就要先分清两类情况症状可能根因脚本文件显示with CRLF line terminatorsWindows行尾符污染脚本是LF干净行尾但sh环境是dash脚本使用了bash专属语法脚本被某个编辑器统一改成全角括号编辑器自动转换导致词法异常3. 从编译日志到手摸到根因完整排查链路3.1 让Arduino IDE把编译细节全部吐出来很多教程遇到编译报错就是一句重装SDCC但我建议你先把编译现场完整记录到本地文件里。Arduino IDE里打开文件-首选项勾选显示详细输出下的编译选项然后重新编译。如果你用的是命令行工具arduino-cli操作更直接arduino-cli compile -b ch55xduino:ch55x:ch552 -v build.log 21日志文件里搜sdcc会看到类似这样一行/home/xxx/hardware/arduino/CH55xDuino/tools/sdcc/bin/sdcc.sh --std-sdcc11 ...这里给出了sdcc.sh的完整路径这就是报错现场的坐标。先确认它存在于该路径再往下走。3.2 把sdcc.sh验明正身的三种命令拿到脚本路径后用下面三个命令给它体检顺序建议从最外层到最内层file sdcc.sh cat -A sdcc.sh | head -20 sed -n 1,20l sdcc.shfile是最直观的。如果输出里出现with CRLF line terminators这几个词基本可以下结论了就是Windows行尾符。cat -A会把行尾的\r显示成^M你看到$前面跟着一大串^M同样坐实问题。sed -n l里那个是小写字母lline不是数字1它会把制表符显示成\t、把行尾显示成$如果每一行末尾都多出一个\r在输出里表现为\r$。如果file输出是干净的ASCII text没有CRLF标记那就要检查第二个方向看/bin/sh指向哪里。ls -l /bin/sh如果输出显示/bin/sh - dash那问题就不是行尾符而是脚本里的bash特性。把platform.txt里调用方式从sh sdcc.sh改成bash sdcc.sh或者在脚本里把所有function foo()写法改成POSIX风格的foo()问题通常就解决了。3.3 常见伪装场景和他们的误判点先说一个容易误判的细节如果脚本没有可执行权限报错会是Permission denied而绝不会是syntax error。反过来你看到syntax error时说明bash已经成功读取了文件内容chmod完全不用管。还有一种伪装场景sdcc.sh本体没问题但脚本内部调用了其他脚本那个被调用的子脚本是CRLF。比如sdcc.sh里有一行source ./sdcc_utils.sh子脚本是Windows行尾报错时bash也会带着sdcc.sh的名字来报误导你先去改父脚本。所以修复前最好把整个tools目录下的.sh文件全列出来逐一file一下别只盯第一个报错文件。再补充一个不算常见但要提的情况某些编辑器会把全角括号和半角括号()混用尤其是从网页复制命令直接粘贴进脚本时。全角括号在bash眼里是普通字符如果它出现在函数名后面同样会引发词法异常。这个在cat -A里看不出来需要直接用编辑器或grep搜索脚本里是否混入了UFF08这类Unicode字符。我用grep -nP sdcc.sh检查过发现过这类奇怪问题。4. 动手修复清理换行符同时给脚本加一层保险4.1 最快修复dos2unix或sed删除\r一旦确认是CRLF修复命令极其简单一条就够了dos2unix sdcc.sh系统里没装dos2unix的话用sedsed -i s/\r$// sdcc.sh这个命令把每一行末尾的\r全部删掉其余字节不动。执行完再用file sdcc.sh复查看到输出里CRLF标记消失理论上就可以重新编译了。我在自己的环境里跑完Arduino IDE重新编译CH552 Blink立刻通过。如果一次改了多个脚本可以批量处理find . -type f -name *.sh -exec sed -i s/\r$// {} \;需要提醒一句以上操作会直接修改文件内容动手前最好先备份原文件。你可以在改之前执行cp sdcc.sh sdcc.sh.bak万一改出问题还能回滚。4.2 不想依赖脚本用platform.local.txt覆盖编译规则如果你觉得这个脚本太脆弱还有个更彻底的做法绕过sdcc.sh直接调用SDCC本体。Arduino核心支持用户级覆盖文件机制你在板卡包目录下新建一个platform.local.txt里面可以覆盖platform.txt里的任意键值。拿我的环境举例platform.txt里编译命令的关键配置是compiler.c.cmd这一项。我创建platform.local.txt写入compiler.c.cmd/usr/bin/sdcc把编译命令直接指向系统SDCC二进制。这样Arduino IDE就不经过sdcc.sh了CRLF问题彻底绕开。但有个大前提必须先搞清楚原来的sdcc.sh到底给SDCC补了什么参数。有些CH55xDuino版本会通过脚本追加--std-sdcc11、-DCH552等选项你直接跳过后这些参数就丢了。稳妥做法是把脚本内容打开读一遍看它最终执行的命令长什么样然后把等价参数写进platform.local.txt的compiler.c.flags或build.extra_flags里。这个方法适合对工具链有一定了解的玩家新手建议还是先用4.1的sed方案改动量最小风险最低。4.3 避免团队协作时换行符灾变的方法修好一次容易要让这个坑不再复发需要配置Git仓库。CH55xDuino是从GitHub拉下来的如果你用Git管理自建核心仓库在仓库根部加一个.gitattributes文件*.sh text eollf这个配置保证无论谁在Windows上git checkout所有.sh文件都会被强制转成LF行尾不会偷偷变成CRLF。类似的.bat、.cmd文件可以用eolcrlf指定为Windows行尾各取所需。这个经验不只对CH55xDuino有效ESP32板卡包、stm32duino、任何含Shell脚本的嵌入式核心仓库都适用。如果项目不用Git纯粹靠压缩包传输那只能靠团队纪律了Windows上编辑脚本文件保存时显式选择LF作为换行符。VSCode在右下角状态栏可以一键切换CRLF/LFNotepad里是编辑-文档格式转换-转为Unix格式。别用Windows记事本去改任何.sh文件这条我用一次文件毁一次脚本确实是个血泪教训。最后还有一个锦上添花的操作改完sdcc.sh后用bash -n sdcc.sh做语法检查。-n表示不执行脚本只检查语法几毫秒就出结果没有输出就代表语法合格。这是防止你把脚本改出更深层语法问题的安全网。5. 别只盯sdcc.sh换行符、缓存、SDCC版本三件套5.1 批量扫描核心目录下的所有脚本sdcc.sh只是第一个受害者。CH55xDuino核心目录下还有ch55x_upload.sh、makebits.sh、ch554prog.sh等一堆辅助脚本它们是烧录、生成bootloader、转换固件时要用到的。如果整个核心包在Windows下被解压过、打包过这些脚本很可能被批量污染。等你修好编译烧录阶段又会冒出ch55x_upload.sh: syntax error near unexpected token那时再排查又是半小时。所以修完sdcc.sh我强烈建议顺手扫一遍整棵目录树find . -type f -name *.sh -exec file {} \; | grep CRLF把所有带CRLF的脚本一次性找出来统一用sed修掉。这样后面烧录、生成配置的阶段都不会再被同一个原因绊倒。5.2 编译缓存残留导致改完不生效这里有一个特别容易误导人的情况你明明改了sdcc.sh重新编译却还是报一模一样的错仿佛文件根本没变。别急着怀疑sed命令没生效先怀疑编译缓存。Arduino IDE会缓存中间构建产物旧文件和旧的编译命令可能被复用了。遇到改了不见效把缓存目录清理一遍——在首选项里能查到构建缓存路径或者按操作系统找Arduino的临时目录把里面的核心缓存、build目录整个删掉。清完之后再编译基本就正常了。这个坑很多人查到最后才发现是缓存惹的祸耗时最长。5.3 SDCC版本差异带来的隐藏故障脚本修好、换行符正常、缓存清空之后如果编译还报别的错比如unsupported option、cannot find file、某些标准库头文件找不到那很可能是系统里装的SDCC和CH55xDuino要求的版本不一致。CH55xDuino对SDCC版本相当敏感不同SDCC版本对C99语法、__sfr、__code这些8051扩展关键字的支持程度不同老版本项目碰到新编译器经常翻车。我见过一个实例有人SDCC版本太高遇到新增的--std-sdcc11和--stack-auto参数组合不兼容报了一堆没头没尾的错误。最后按CH55xDuino官方文档指定的SDCC版本装回4.0系列问题全部消失。所以排查时确认一下板卡包README里要求的SDCC版本顺便用sdcc --version看看系统实际版本尽量保持一致。这是很多排查指南里不会提的细节但实战中概率不低。5.4 改不动脚本时的手动编译备选路子如果脚本已经被改成无法挽回的状态又不想大费周章重装板卡包还有一个压箱底的备选方案干脆绕开Arduino IDE的编译引擎从详细日志里把最终编译命令复制出来手动执行。日志里会显示类似这样的完整命令sdcc --std-sdcc11 -mmcs51 -pCH552 -DCH552 -DF_CPU24000000L -I... -o build/sketch.hex ...把这些参数里的{source_file}、{build.path}变量用实际路径替换掉然后在终端里手动跑一遍。这个方法麻烦但有两个价值一是能精确看到sdcc.sh最终传给SDCC的每一个参数二是当你怀疑脚本哪里出错时手动跑一遍就能逐步验证是哪一段逻辑出了岔子。我排查复杂问题的时候反而最喜欢用这个方式因为IDE的日志会吞掉很多中间环节手动执行则把一切都暴露出来。回到开头的场景我现在再看到sdcc.sh: syntax error这类报错已经养成一套肌肉记忆先file一下脚本再cat -A看行尾最后才去查代码逻辑。大多数时候问题就藏在一个看不见的回车符里。CH55xDuino这套玩法本身挺有价值——把几块钱的USB单片机搬进Arduino生态能用熟悉的API写USB设备、模拟键盘、做HID调参器踩坑是难免的。但编译链这个坑一旦趟平后面CH552点灯、CH554模拟USB键盘、CH559做双串口透传就顺多了。至少在你下次看到某个.sh脚本报unexpected的时候不会再第一时间怀疑SDCC本体了。