先说我这里的结论:CH55xDuino 在 Arduino IDE 里编译报sdcc.sh: syntax error: unexpected "(",九成以上不是你的代码写错了,也不是开发板没选对,而是工具链里的 shell 脚本在拼装命令前就被解析器干掉了。第一次遇到这个报错的人,很容易被 Arduino 输出面板里几百行红色日志带偏,其实真正有用的信息就前几行。我前前后后帮人排查过很多回,自己也中过招,这篇文章把这类问题的根源、排查套路、修复方法一次讲清楚。如果你正好在 Ubuntu、WSL 或者 macOS 上玩 ch552/ch551 这类小板子,编译时见过类似报错,直接照着抄作业就行。
1. 先看懂 sdcc.sh 在编译流水线里到底站哪个位置
1.1 CH55xDuino 的特殊之处:它不用 avr-gcc
玩 Arduino 的人都知道,官方板子用的是 AVR 工具链,编译器叫 avr-gcc。但 CH55xDuino 这个第三方核心支持的 CH551、CH552、CH554、CH558、CH559 这些芯片,内核其实是增强型 8051,不是 AVR。8051 这套体系有自己的 C 编译器,最常用的开源方案就是 SDCC(Small Device C Compiler)。
SDCC 和 avr-gcc 完全是两套东西,指令集、寄存器、段布局全不一样,所以 Arduino 官方的编译套路对它完全无效。CH55xDuino 的作者做了两件事:一是把 SDCC 针对 CH55x 系列重新编译了一份,让芯片型号、Flash 区段、USB 相关寄存器这些定义都齐全;二是写了一个封装脚本,让 Arduino 的构建系统能像调用 avr-gcc 一样调用 SDCC。这个封装脚本在 Linux 和 macOS 上就是 sdcc.sh。
能把成本压到一两块钱、还自带 USB 的板子,CH552 在智能小车、USB 小键盘、传感器采集这些项目里出镜率很高。但便宜有便宜的道理,工具链的坑也相对多一些,这个 sdcc.sh 报错就是典型的坑。
1.2 从点下“编译”到报错,中间隔了一个封装脚本
很多人以为点下 Arduino IDE 的编译按钮,就是 IDE 自己把代码变成立即烧录的 hex 文件。真实流程比这绕:Arduino IDE 把任务交给 arduino-cli 的构建器,构建器读取开发板核心里的 platform.txt,platform.txt 里写好了每一步编译命令模板,也就是所谓 recipe。CH55xDuino 的 recipe 里,编译器路径指向的不是裸的 sdcc 命令,而是那个 sdcc.sh。
这个封装脚本存在的理由很实在:它负责补全 SDCC 的 include 和 lib 路径,让工具链不管被装到哪个用户目录下都能找到自己的头文件和库;同时处理 macOS 和 Linux 下可执行文件路径的差异。也就是说,sdcc.sh 相当于一个翻译层,把 Arduino 传过来的一堆参数整理好后再转交给真正的 SDCC 二进制。脚本一旦在解析阶段出问题,后面什么都做不了。
这也是为什么这个报错看起来特别“早期”:它发生在编译过程最开始,甚至还没到语法分析你的代码那一步。你的 .ino 代码再怎么正确,工具链的脚本坏了就是坏了。
1.3 报错到底长什么样、什么时候冒出来
在 Ubuntu 上用 Arduino IDE 2.x 编译 CH55xDuino 的任意示例程序,输出面板会刷一大片红色,开头大概是这种风格:
/home/你的用户名/.arduino15/packages/ch55xduino/tools/sdcc/4.0.0+ch55xduinobl/sdcc.sh: 1: Syntax error: "(" unexpected不同发行版、不同 shell 的措辞略有差异,有的写syntax error: unexpected "(",有的写Syntax error: "(" unexpected,认准里面同时出现 “syntax error” 和括号就对了。这个报错的特点是:不管编译哪个例程,哪怕是个空的 Blink,都会稳定复现;而且通常从第一次配置好开发板、第一次编译就炸。如果你是在某个集成环境里搜错误信息,大概率看到的解释都指向 shell 语法问题,方向是对的,但具体是哪一种 shell 语法问题,很多人没说透。
2. syntax error 的三种常见根源
2.1 头号嫌疑:CRLF 换行符
Unix 系的 shell 脚本有个很老的规矩:行尾只能有一个换行符 LF,也就是\n。而 Windows 文本编辑器默认存的是 CRLF,也就是\r\n,行尾多了一个回车符\r。问题就出在这。
shell 解析脚本时是按字节流的,行尾的\r也会被当成内容。举个例子,脚本里如果有函数定义:
build_flags() { echo "hello" }CRLF 版本在 shell 眼里其实是:
build_flags\r() {\r ...build_flags后面粘了一个不可见的\r,dash 解析器把build_flags\r当成一个普通 WORD,接着看到紧跟的(,发现这个位置根本不该出现括号,于是一口咬定:Syntax error: "(" unexpected。这个机制完美解释了为什么报错会指向括号。
对比一下现实生活:就好比一份蛋糕配方里混进了一颗螺丝,机器不会告诉你螺丝在哪,只会在打发奶油那一步突然卡死。\r就是那颗螺丝,报错里的括号只是它绊倒的地方。
2.2 二号嫌疑:dash 和 bash 的语法分歧
第二个常见根源是“脚本写的是 bash 语法,却被 sh 拿去解析了”。Debian 和 Ubuntu 系的 Linux,/bin/sh并不是 bash,而是 dash,一个更精简、更严格的 POSIX shell。你可以在终端验证:
ls -l /bin/sh很多系统上输出是指向 dash 的软链接。dash 不接受 bash 的某些专属语法,其中最典型的就是function关键字。比如脚本里这么写:
function build_flags() { echo "hello" }bash 认得这个东西,dash 不认。dash 把function当成一个普通命令名,接着又读到一个build_flags,两个连续的 WORD 之间突然冒出一个(,于是同样报Syntax error: "(" unexpected。除此之外,[[ ]]测试、数组声明这类 bash 语法,dash 也一律不认。
CH55xDuino 的官方包在 macOS 上测试得比较多,而 macOS 的 sh 是老的 bash,对语法宽容一些。同一个包在 macOS 上没事、在 Ubuntu 上炸,很多时候就是 sh 解释器不同造成的。
2.3 三号嫌疑:文件被污染或下载不完整
第三种情况是 sdcc.sh 文件本身已经不完整了,最常见的是下载过程出了问题。Arduino 的 Board Manager 拉工具链走的是网络,如果网络环境复杂,返回的可能不是真正的压缩包,而是某个错误提示页面。这种文件被解压后,sdcc.sh 里全是些 HTML 标签或者缺行半句,shell 自然解析不过去。
另外还有 BOM 头的问题。Windows 下某些编辑器会往文件头部塞三个字节的 UTF-8 BOM(EF BB BF)。正常情况下这也能让 shell 懵一下,虽然报错形式可能不是括号,而是直接把 BOM 当命令执行。检查方法很简单,用xxd看一眼文件开头的十六进制就知道了。
判断这类问题的核心原则:工具链目录下的 sdcc.sh 不是你写的,你也没有动过它,那么它“突然坏了”的概率其实很低;“从下载到解压这条链路出了问题”的概率反而高。
2.4 为什么同是一个包,有人没事你炸
这一点最让人困惑:同一个 CH55xDuino,同一份教程,别人编译一次过,自己就报错。原因通常不是官方包本身有问题,而是工具链到你机器上的路径不一样。
官方发布的 Linux 工具链包,脚本行尾本来就是 LF,正常从 Board Manager 拉下来、在 Linux 里解压,是没事的。但如果你在 Windows 上下载了压缩包,用 Windows 解压工具解过,再拷到 Linux 里用;或者用 Git 在 Windows 上克隆过仓库,Git 又开着core.autocrlf=true;再或者用某个“绿色搬运版”IDE 集成环境,中间任何一步都可能让脚本的换行符变成 CRLF。这也是为什么这个报错在 Windows 原生环境里很少见——Windows 上 CH55xDuino 调的是 sdcc.exe,sdcc.sh 是给 Linux/macOS 用的;常见出事场景集中在 WSL、Git Bash、双系统共享文件、或者 Windows 拷文件到 Linux 虚拟机。
3. 从复现到修复的完整排查流程
3.1 第一步,把 sdcc.sh 的真实路径找出来
别在 Arduino 的输出面板里找路径,太费眼睛,直接在终端里把整个工具链目录定位出来。Linux 和 macOS 下 Arduino 2.x 的用户目录一般在~/.arduino15,用 find 扫一遍最快:
find ~/.arduino15/packages/ch55xduino -name "sdcc.sh" 2>/dev/null正常会得到一个类似这样的路径:
/home/你的用户名/.arduino15/packages/ch55xduino/tools/sdcc/4.0.0+ch55xduinobl/sdcc.sh这里有个小细节:版本号目录带加号,比如4.0.0+ch55xduinobl。手动敲路径时记得整体复制,别把加号漏了或者被终端当成什么特殊符号。如果你用的是 Arduino 1.x 的绿色便携版,工具链可能在 IDE 安装目录下的portable/packages里,用 find 一样能扫到。
3.2 第二步,绕开 Arduino 手动复现报错
找到脚本后,cd 进目录手动跑一遍,把报错从 Arduino 里“请”出来:
cd ~/.arduino15/packages/ch55xduino/tools/sdcc/4.0.0+ch55xduinobl/ ./sdcc.sh --version如果直接执行就报错,再换一种方式:
sh ./sdcc.sh --version这两种执行方式的区别在于:第一种走的是脚本自己的 shebang(第一行声明的解释器),第二种是你强制指定sh去解析。对比这两个结果,能大致判断问题是出在“解释器指向错了”还是“脚本内容本身解析不了”。如果两种方式都报syntax error: unexpected "(",那几乎可以锁定是脚本内容本身出了问题,优先查换行符,其次查 bash 专属语法。
3.3 第三步,验证换行符和编码
在脚本目录里执行:
file sdcc.sh如果输出里带有with CRLF line terminators,那基本实锤了,就是 Windows 换行符。再配合cat -A看行尾更直观:
cat -A sdcc.sh | head -20正常 Linux 脚本每行末尾只有一个$,$就是 LF 的可视化表示;如果看到很多^M$,说明每一行都带着\r,也就是^M。这个命令输出的画面很直观,一堆^M$摆在眼前,基本不用怀疑其它原因了。
顺带看一眼文件前面的内容,确认没有奇怪的字符;再用head -1 sdcc.sh看 shebang。如果开头不是正常的#!/bin/sh或者#!/bin/bash,而是混了 BOM 或其他内容,问题就又多一层。
3.4 第四步,对症下药修脚本
针对 CRLF,Linux/macOS 下一条 sed 搞定:
sed -i 's/\r$//' sdcc.sh这条命令的意思是:把每行行尾的\r删除,$代表行尾锚点,所以不会误伤行中间的回车字符。执行完再跑一遍file sdcc.sh,确认 CRLF 字样消失,然后补上执行权限:
chmod +x sdcc.sh接下来做语法健康检查:
sh -n sdcc.sh && echo "sh 语法OK" bash -n sdcc.sh && echo "bash 语法OK"-n是只检查语法不执行的模式,非常安全。如果sh -n报错而bash -n通过,说明脚本里有 bash 专属语法,而系统的 sh 是 dash。这时候可以把 shebang 改成 bash:
sed -i '1s|^#!/bin/sh.*|#!/bin/bash|' sdcc.sh如果不想改脚本,也可以直接装一个 bash 兼容层,但改 shebang 是最直接的。改完再跑一次./sdcc.sh --version,只要是这个封装脚本本身没问题,通常能正常输出 SDCC 的版本号信息。
3.5 第五步,回到 Arduino 做最终验证
脚本能手动跑通之后,回到 Arduino IDE 重新编译。建议先把编译输出切到“详细”模式,确认这次调用的还是同一个 sdcc.sh,并且没有报错。
如果编译还是失败,而且报错位置指向别处,那就跟你本次的问题无关了,按新报错继续查。如果手上有 arduino-cli,也可以直接用命令行验证,输出更干净:
arduino-cli compile --fqbn ch55xduino:ch55xduino:ch552 你的工程目录FQBN 里的板型名以你实际安装的核心版本为准,ch552、ch554 对应条目不同。命令行方式最大的好处是每次构建都是独立进程,不受 IDE 缓存干扰。
4. 顺带踩过的其它坑和报错速查表
4.1 工具链没权限,别急着 sudo
和 syntax error 相邻的高频问题,是Permission denied。这两个经常被放在一起问,但病因完全不同。Permission denied是脚本解析正常、但文件没有可执行权限,目录下执行:
chmod +x sdcc.sh就能解决。千万别一上来就sudo sdcc.sh,sudo 会把当前用户的环境变量和路径搅乱,导致工具链找不到自己的头文件,产生一堆新的奇怪报错。在 Arduino 这个场景里,遇到权限问题就给用户目录下的文件加权限,而不是提权。
4.2 下载过程被污染才是真暗坑
有一种情况比较阴:sed 改了、权限也加了、语法检查也过了,但编译还是报错,而且这次报的是sdcc: command not found或者干脆说No such file。这时候要检查真正的 SDCC 二进制文件是否完整:
file sdcc正常情况下 Linux 下输出的应该是ELF 64-bit LSB executable之类的信息。如果输出是ASCII text,说明这个叫 sdcc 的文件根本不是可执行程序,而是一段文本——十有八九是当初下载的时候,某个环节把响应内容换成了错误提示页。这种时候就别修了,直接重装工具链。
排查思路其实就一句话:先确认文件类型对不对,再谈修脚本。文件类型都不对,后面全是白用力。
4.3 Windows、WSL、macOS 三个环境三个脾气
Windows 原生 Arduino 2.x 上,CH55xDuino 调用的是 sdcc.exe,基本不碰 sdcc.sh,所以看到本文写的 CRLF 问题时先确认自己是不是在 WSL 或者 Git Bash 里。WSL 里装的 Ubuntu 自带 dash,最容易触发unexpected "(";Git Bash 对换行符的容忍度居中,但也不建议赌。
macOS 又是另一个脾气:它的/bin/sh是老版本 bash,对 CRLF 和部分 bash 语法都比 dash 宽容,所以同一个包在 macOS 上编译正常、拿到 Ubuntu 上就炸,是常见的跨平台现象。如果你在 macOS 上遇到了同样的报错,优先怀疑下载不完整和权限问题,其次才是换行符,因为 macOS 下 CRLF 被容忍的概率高很多。
4.4 报错信息速查表
| 报错特征 | 大概率原因 | 处理方式 |
|---|---|---|
syntax error: unexpected "(" | CRLF 换行符,或 bash 语法被 dash 解析 | sed -i 's/\r$//' sdcc.sh,必要时改 shebang |
Permission denied | 文件无执行权限 | chmod +x sdcc.sh |
command not found,且命令名后带^M | CRLF 换行符 | 同上 sed 转 LF |
No such file/sdcc: command not found | 工具链下载不完整、二进制缺失 | 重装工具链 |
| 脚本内容全是 HTML 或乱码 | 下载被污染 | 删除包目录重新下载 |
这张表基本覆盖了 CH55xDuino 工具链相关的常见报错,遇到不相识的错误时,先看它属于“解析问题”还是“文件缺失问题”,方向对了事半功倍。
5. 不想下次再遇到,从源头管住这几件事
5.1 Git 的 autocrlf 设置
如果你是那种喜欢从 GitHub 克隆 CH55xDuino 源码自己折腾的人,Windows 下 Git 默认的core.autocrlf=true会在 checkout 时把 LF 自动转成 CRLF,这就是脚本被“传染”的经典路径。建议在克隆仓库前设置:
git config --global core.autocrlf input这个设置的意思是:提交时转成 LF,checkout 时不强行转 CRLF,对 shell 脚本最友好。已经克隆下来的仓库,也可以先跑一遍sed -i 's/\r$//'把已有文件洗干净。
5.2 编辑器和文件传输习惯
如果哪天你需要手动查看或者修改 sdcc.sh,记住三条:不要用 Windows 记事本打开保存;VS Code 右下角把行尾模式切到 LF;从 Windows 往 Linux 传文件,优先用压缩包传输而不是直接拷贝散文件。最后一条很多人容易忽略:散文件直接拷到 Linux,换行符和权限位都可能变;打包成 tar.gz 再传,至少换行符是原样保留的。
5.3 重装工具链这个“核弹级”方案
如果你不想深究原因,只想赶紧把代码跑起来,最省心的方案是让 Arduino 重新拉一遍工具链:
- 在 Arduino IDE 的“开发板管理器”里搜索 ch55xduino,卸载它;
- 手动删掉
~/.arduino15/packages/ch55xduino整个目录; - 在“文件 → 首选项 → 附加开发板管理器网址”里确认 CH55xDuino 的 JSON 地址还在,格式类似
https://raw.githubusercontent.com/deqing/CH55xDuino/master/package_ch55xduino_english_index.json; - 重新安装。
这一步本质是把所有可能被污染的文件一次性换新,官方重新下载的 Linux 工具链换行符就是正常的。代价是费点流量和时间,收益是干净环境。我自己的习惯是:如果 sed 修完 5 分钟内没解决,就直接走这条重装路线,不纠结。
5.4 把修复自动化,一劳永逸
如果你像我一样要维护多台机器、多个环境,或者隔三差五帮人修,把修复脚本写成一个小文件,以后换机器一键执行:
#!/bin/bash SDCC_SH="$(find ~/.arduino15/packages/ch55xduino -name sdcc.sh 2>/dev/null | head -n1)" if [ -n "$SDCC_SH" ]; then sed -i 's/\r$//' "$SDCC_SH" chmod +x "$SDCC_SH" echo "已修复: $SDCC_SH" sh -n "$SDCC_SH" && echo "语法检查通过" else echo "没找到 sdcc.sh,请先确认 CH55xDuino 已安装" fi这个脚本放在任何一台 Linux 机器上都能跑,逻辑很简单:找到脚本、洗掉 CRLF、补权限、做语法检查。实测下来,这套组合拳能解决绝大多数 sdcc.sh 报错场景。
根据我自己的经验,这类问题里 CRLF 换行符至少要占七八成,剩下两成是下载污染和权限。说句掏心窝的话:任何 shell 脚本只要经过 Windows 环境过一趟手,你都该先file一下再跑,别指望它是干净的了。换行符这东西在脚本世界里就是个隐形刺客,平时看不见,关键时刻一击致命。最后再分享一个小技巧:修完工具链之后,顺手把整个4.0.0+ch55xduinobl目录打一个 tar.gz 备份,下次再遇到类似问题,直接解压覆盖,连 re-download 的流量都省了。