如果你和我一样,手头攒了一抽屉CH552、CH551这种小板子,想蹭Arduino的生态,又实在不想回到Keil那套老旧的界面,那CH55xDuino几乎是绕不开的选择。它把增强型8051内核的CH55x系列芯片包装成了Arduino开发板,编译后端默认是SDCC编译器。这套组合在Windows上有一个很经典的下马威:第一次点下编译按钮,进度条还没走两步,控制台就甩出一行sdcc.sh: syntax error: unexpected "(",然后整个编译流程直接中断。
这篇文章我就围绕这个报错,把它的背景、成因、完整排查链路、修复操作和防复发手段全部摊开讲。如果你的报错一模一样,直接跳到第3节看修复命令;如果你想搞明白为什么会在Windows上冒出这样一个像外星代码的shell语法错误,建议从第1节顺序读下去,后面每一个步骤我都给了能直接照抄的命令。
1. 先复盘这个报错出现的完整背景
1.1 CH55xDuino 和 sdcc.sh 到底是什么关系
CH55x系列是8位增强型8051内核芯片,而Arduino官方工具链默认是给AVR系列写的,里面的avr-gcc根本不认识8051的指令集。所以CH55xDuino这个第三方核心包做了两件事:第一,把Arduino的API精简移植到CH55x上;第二,引入SDCC(Small Device C Compiler)作为真正的编译驱动。
SDCC本身是一个多平台开源的C编译器,但在Windows上它并没有被包装得那么“Windows化”。CH55xDuino的作者在hardware/ch55x/tools/目录里放了一个sdcc.sh脚本,让Arduino在编译时通过这个脚本来做路径定位、参数拼接,最终再调用SDCC真正的编译程序。
你可以把sdcc.sh理解成编译流水线上的扳道工。它不是编译核心,但少了它,整列火车就是开不到目的地。Arduino每次编译CH55x的工程,都会先在platform.txt里找到编译recipe,然后启动一个shell去执行sdcc.sh,这个脚本再根据当前系统判断SDCC的可执行文件在哪、该传哪些参数。
在Linux和macOS上这套流程很顺畅,因为系统本身自带shell。但在Windows上,shell这个东西是额外装的,一旦执行环境或者脚本文件格式出了问题,就会炸出你看到的那个语法错误。
1.2 我的复现环境与触发步骤
复现这个报错不需要什么特殊条件。我当时的环境是Windows 11,Arduino IDE 2.3.x,板卡选的CH552,CH55xDuino是用git clone方式装进用户目录的:
git clone https://github.com/DeqingSun/CH55xDuino.git然后把整个CH55xDuino目录放到%LOCALAPPDATA%\Arduino15\packages\CH55xDuino\hardware\ch55x,或在sketchbook的hardware目录下建立一个ch55x同名目录。
打开Arduino IDE,在开发板管理器里看到CH55x系列选项后,选一块CH552,把Blink样例编译一下,大约两三秒后Arduino IDE的编译输出窗口就开始刷出一长串日志,然后最显眼的位置出现了这行报错:
Forking /bin/sh -c "C:\Users\xxx\AppData\Local\Arduino15\packages\CH55xDuino\hardware\ch55x/tools/sdcc.sh" chip sdcc.sh: syntax error: unexpected "(" exit status 1 Error during build: exit status 1exit status 1是Arduino把shell进程的退出码原样抛了出来,真正要命的是sdcc.sh: syntax error: unexpected "("这一行,它来自shell解释器,说明shell在解析sdcc.sh文件内容的时候就挂了,根本没走到编译那一步。
2. 核心问题:sdcc.sh 为什么会触发 syntax error
2.1 从编译日志看 sdcc.sh 的调用上下文
Arduino在调用外部编译工具时,不是直接执行sdcc.sh文件,而是执行类似下面这样一条完整命令:
"C:\Program Files\Git\bin\sh.exe" -c "C:\Users\xxx\AppData\Local\Arduino15\packages\CH55xDuino\hardware\ch55x/tools/sdcc.sh" chip注意这里的关键信息:是sh.exe去解析并执行sdcc.sh,不是cmd.exe。也就是说,你的机器上一定要有一个可用的sh类解释器,Arduino才能完成这个调用。大多数情况下这个解释器来自Git for Windows,也可能是MSYS2或者WSL。
报错里的unexpected "("来自shell的语法解析器。bash/sh这类shell在读取脚本时,会按自己的语法规则逐行解析,如果遇到某个token出现在它预期之外的位置,就会抛syntax error。小括号(在shell里有特定含义,要么是子shell的边界,要么是函数定义的参数列表起始符,要么是数组或命令替换的特殊语法。如果出现在不该出现的地方,解释器就直接说看不懂,拒绝继续执行。
2.2 引发 unexpected "(" 的最常见根因
在Windows环境下,第一个要怀疑的就是脚本的换行符。GitHub上CH55xDuino仓库里的所有.sh文件本来都是Unix风格的LF换行,但Windows用户git clone时,如果git全局配置了core.autocrlf=true,git会自动把这些文件在checkout时全部改成CRLF换行,也就是Windows记事本最常写的那个格式。
问题在于,shell解释器不认识CRLF里的那个\r回车符。一个正常的脚本行末尾应该只有一个\n,CRLF版本会变成\r\n,shell读到\r时会把它当成某个命令或参数的一部分。比如脚本里有这么一行:
CC="$(dirname "$0")/sdcc/bin/sdcc"如果是CRLF,这行实际解析出来是:
CC="$(dirname "$0")/sdcc/bin/sdcc"\rshell在处理\r的时候,会被它误导,常见表现就是某些括号、引号状态判定错乱,最终在语法解析阶段就报出unexpected "("。
第二个根因是脚本自带BOM头。如果某个工具或编辑器把脚本存成了UTF-8 with BOM,BOM字符会出现在第一行的#!/bin/sh前面。shell看到#!时本来认得这行是指定解释器的,但前面多了一个不可见字符,这个识别就会失效,连锁反应可能报出各种怪异的语法错误。
第三个可能性是脚本本身用了bash特有语法,而执行它的sh解释器其实是dash。这个在纯Windows下相对少见,但不是没有,有些精简的shell环境对括号、[[ ]]等语法支持不完整。CH55xDuino官方脚本用的是POSIX兼容写法,正常情况下不会踩这个坑,但如果你的副本被改动过,就要注意。
2.3 先拿到三份证据再动手
修复之前,我们要在Git Bash或者WSL里先确认问题到底出在哪,别一上来就乱改。我用这些命令做了检查:
cd <你的ch55x/hardware/ch55x/tools目录> file sdcc.sh head -c 16 sdcc.sh | od -A x -t x1z sed -n '1p' sdcc.sh | cat -Afile命令会直接告诉你文件的换行风格。如果输出里包含with CRLF line terminators,那基本就是它了。od命令用来看文件开头的字节,如果开头是efbbbf,说明带着UTF-8 BOM。cat -A会把行尾的回车符显示成^M$,一目了然。
这三份证据能帮你精确区分:是CRLF的问题,还是BOM的问题,又或者是脚本第一行被改坏的问题。下面第3节的四个方案就是按照证据导向来的。
3. 从最可能到最隐蔽,四种排查与修复方案
3.1 方案一:用 sed 一次性修复换行符
如果确认是CRLF,直接对sdcc.sh做一次换行符归一化就可以了。在Git Bash里执行:
sed -i 's/\r$//' sdcc.sh这个命令的作用是把每一行行尾的\r字符删掉,保留\n。如果你手头有dos2unix这个工具,也可以一步到位:
dos2unix sdcc.sh但更实用的建议是不要只修这一个文件。CH55xDuino包里可能还有其他.sh脚本,虽然触发报错的是sdcc.sh,但同一套CRLF转换是均匀作用在每个文件上的,今天我帮你修好了sdcc.sh,改天它还可能去执行另外的脚本然后继续炸。我直接把整个tools目录下的所有.sh文件统一修复:
find . -name "*.sh" -exec sed -i 's/\r$//' {} \;这条命令会递归查找当前目录下所有.sh文件,把它们全部切掉CR。执行完以后再用file sdcc.sh确认输出变成了ASCII text,没有再跟CRLF字样,然后回到Arduino IDE重新编译。
3.2 方案二:清掉BOM和首行的隐藏字符
如果你检查下来发现换行符本身没问题,但head -c 16 sdcc.sh | od里看见了efbbbf,那就要处理BOM。这个情况多半是有人在Windows上用记事本或某个国产编辑器打开脚本后另存为“UTF-8 BOM”格式造成的。
清除BOM的命令是:
sed -i '1s/^\xEF\xBB\xBF//' sdcc.sh它只对第一行做处理,把开头的efbbbf三个字节删掉。处理后再用od确认开头已经是正常的23 21,也就是ASCII字符#!。
BOM问题比较阴险,因为你在GUI编辑器里看起来脚本完全正常,但shell解析就是不行。我见过有人为这个问题重装了三次CH55xDuino,最后发现是编辑器自动加了BOM。
3.3 方案三:确认Arduino到底用的是哪个sh
这个报错能出现,说明Arduino至少找到了一个sh解释器。但如果你机器里同时装了多个shell环境,比如WSL、Git Bash、MSYS2,Arduino到底拿哪个来跑脚本,有时候并不直观。
可以在Git Bash里执行:
where sh where bash看看系统PATH里Shell的实际路径。我用的是Git for Windows自带的sh,路径在C:\Program Files\Git\bin\sh.exe。如果你把sh.exe放到了一个带中文空格或括号的路径下,那么Arduino生成命令行时拼接出来的引号可能有别扭,也会造成奇怪的syntax error,虽然这个概率比CRLF低不少。
如果where sh什么也没找到,说明你的机器上压根没有sh。这时候Arduino的报错通常会变成Cannot run program "sh",但既然已经出现了syntax error字样,说明sh是存在的,那就重点怀疑脚本格式而不是环境。
3.4 方案四:彻底重装核心包,绕开手工clone
如果你不想折腾脚本内容,最干净的办法是放弃git clone的安装方式,改用Arduino官方开发板管理器来装CH55xDuino。先在Arduino IDE里配置额外的板卡索引地址,填上CH55xDuino发布页里那个package_ch55xduino_index.json地址,然后在开发板管理器里搜索ch55x,一键安装。
这种安装方式从发布包直接下载zip,zip里的文件不会像git clone那样按你的git配置做自动换行转换,所以几乎不会出现CRLF问题。用arduino-cli的话也是一回事:
arduino-cli core update-index --additional-urls https://.../package_ch55xduino_index.json arduino-cli core install ch55xduino:ch55x如果你对网络环境有顾虑,也可以手动下载对应的release zip,解压后放到hardware目录。这样既保留了离线安装的确定性,又绕开了git的换行污染,是我现在比较推荐的安装路径。
4. 修复之后的验证与烧录实战
4.1 用 arduino-cli 做一次干净的编译验证
修好脚本后,别急着回IDE点按钮,我建议先用arduino-cli做一次静默编译,日志更干净,也更容易判断是真修好了还是运气好绕过去了。
先把arduino-cli装好,然后找到你刚才编译失败的样例目录,执行:
arduino-cli compile --fqbn ch55xduino:ch55x:ch552 你的样例目录FQBN里的ch552要根据实际板子改成ch551、ch554或ch559。如果一切正常,日志末尾会出现编译生成的文件路径,并且有类似Compilation successful的提示。用命令行编译还有个额外好处:它不依赖IDE的GUI缓存,可以避免Arduino IDE里那些偶尔出现的“伪报错”。
我当时修完整包后跑一次编译,还顺手把每个.sh文件都用file扫了一遍,确保没有漏网之鱼。偶尔会有一种情况:核心脚本是LF了,但IDE的编译缓存还保留着旧的错误状态,这时候重启Arduino IDE再试,基本就稳定了。
4.2 成功之后烧录环节的几个雷区
编译通过只是第一步。把编译好的hex烧进CH552时,还要先搞清楚板子处于什么状态。CH55x在出厂时通常带一个USB bootloader,烧录需要让芯片在启动时进入bootloader模式。不同的板子进入方式不一样,有的是按住DOWNLOAD键再插USB,有的是把P3.1引脚拉低再上电,具体看你的板子设计。
CH55xDuino配套的烧录脚本通常叫wchisp,它是通过USB直接和芯片的bootloader通信的,不需要额外的USBASP硬件。烧录命令大致是这样:
wchisp flash -f 你的固件.hex如果上传失败,先别骂编译器。看看系统设备管理器里CH55x有没有正确枚举成一个COM口或者USB设备,很多时候其实是驱动没装好,或者bootloader被以前的测试程序覆盖了。另外,CH552这种芯片内部的闪存不大,如果你在Arduino里打开了过多的库和字符串常量,编译出的hex常常超过容量,烧录工具会报错,这属于正常的资源限制。
还有一个我在实操中经常踩的坑:CH55xDuino在Windows上若是用板载USB串口进行烧录,容易被系统里其他串口工具占用。烧录前先关掉串口监视器和其他占用串口的软件,否则wchisp会一直提示打不开设备,看起来很像编译或烧录配置有问题。
5. 如何在未来彻底避开这类问题
5.1 Git的autocrlf才是真正的源头
这次遇到syntax error,根子上是git的换行符自动转换。很多Windows用户的全局git配置是core.autocrlf=true,这个配置的本意是好的,它在checkout时把仓库里的LF转成CRLF,让Windows记事本打开文本文件时更友善;在提交时再转回LF,避免仓库被CRLF污染。
但问题恰恰出在这个“本意”上。.sh脚本是给Unix shell执行的,它对CRLF零容忍。git不知道这个文件是脚本,它只知道“这是个文本文件,帮你转成Windows风格吧”,于是就把sdcc.sh转成了CRLF,然后shell炸了。
所以我建议把全局配置改成false,一劳永逸:
git config --global core.autocrlf false git config --global core.safecrlf truecore.safecrlf的作用是,git在做可能引起混乱的换行转换时给出警告,相当于多了一层保险。改完配置后,之前已经clone下来的CH55xDuino还需要重新拉一遍才能恢复LF文件,最省事的办法是删掉重新clone。
5.2 给仓库加 .gitattributes 做强制约束
如果CH55xDuino仓库里原本就写了.gitattributes,我在Windows上clone的时候就不会碰到这个坑了。这个文件的内容很简单:
* text=auto *.sh text eol=lf它的意思是:所有文本文件按自动规则处理,但所有的.sh文件强制使用LF换行。有了这个声明,不管用户本地的git配置怎么折腾,checkout出来的.sh文件始终是LF,shell再也不会被\r搞糊涂。
这个文件对项目作者来说是治本的良药,对我这种下游用户来说,能做的是在clone下来后顺手检查一下仓库里是否已经带了.gitattributes。如果作者没有加,你可以自己在本地手动加一个放在仓库根目录,至少能防止后续的git操作再次破坏这些脚本。
在团队协作或者多人维护的场景里,.gitattributes比在群里吼“大家把换行符改成LF!”有效得多。因为它是git层面的规则,每个成员clone出来就已经是正确格式了。
5.3 两个值得长期坚持的小习惯
第一个习惯是,修改任何.sh脚本之前,先执行file 脚本名,确认文件是LF还是CRLF。很多人在调试脚本时习惯在Windows上用一个带自动换行转换的文本编辑器打开文件,改完保存,问题就回来了。改shell脚本,宁可编辑后明文检查一遍,也不要让编辑器帮你做“人性化”的换行处理。
第二个习惯是,固定用一种安装路径。我见过最多反复报错的人,都是这次用board manager装,下次为了更新又用git clone装,两边版本一混,目录结构对不上,脚本也被各种姿势改过。CH55xDuino这类第三方核心包,我现在的习惯是只用release zip手动解压,不用git clone,因为zip里的换行符是发布者当时提交的状态,不会被本机配置二次改写。
如果你已经用了git clone方式,并且已经触发过一次语法错误,那修完这次之后,最好把这个目录加入你日常编译环境里的“只读区域”,不要顺手把整个hardware目录交给dropbox、坚果云这类云同步工具。云同步在后台对文件的复制和恢复,有时候会把换行符搞回去,这种环境下出现的诡异报错往往比今天这个更难排查。我的CH55xDuino核心包现在放在本地固定目录,不参与任何云同步,这半年再没碰见过syntax error: unexpected "("这种问题。