SDCC 是一个小型设备的 C 语言编译器,该编译器支持标准 C 语言;相对于 GCC 编译器来说可能知名度不是很高,但它跟 GCC 一样,是跨平台,并且遵循 GPL 开源协议。本次实验是使用 nuvoton 的 MS51 系列单片机来操作(基于 8051 内核)

sdcc 官方网址:http://sdcc.sourceforge.net/

Wiki 主页:https://sourceforge.net/p/sdcc/wiki/Home/

# 关于 SDCC

SDCC 是可重定目标的、优化的标准 C(ANSI C89,ISO C99,ISO C11)编译器套件,针对的是基于 Intel MCS51 的微处理器(8031、8032、8051、8052 等),Maxim(以前为达拉斯)DS80C390 变体,飞思卡尔( 基于 HC08(hc08,s08),基于 Zilog Z80 的 MCU(z80,z180,gbz80,Rabbit 2000/3000,Rabbit 3000A,TLCS-90),Padauk(pdk14,pdk15)和 STMicroelectronics STM8。

在安装了 SDCC 后,通过指令查看版本号可以看到它所支持的设备类型:

image-20201027210643106

然后,这里有个帖子有讨论 SDCC 的一些相关东西,而且好像(我也不确定)SDCC 的开发者也在里面,感兴趣的可以看一下:https://wap.newsmth.net/article/905eb27dddf829f15c81077215d66284?title = 电路设计与调试 & from=search

SDCC 较于 Keil 来说,它对 C 语法的严谨度是很高的,更像一个标准的 C 语言编译器,并不会像 Keil 那样把一些 warning 去除掉,自动帮你优化;前面说了, SDCC 是一个好的编译器,可优化方面稍微有点不够完美,以至于代码生成的体积还是比 Keil C51 大一些(是不是我还有些优化指令没 get 到呢?)。

# 安装及环境配置

1、 SDCC

软件的下载路径在上面的 sdcc 主页上有对应的接口,只需要下载相关的平台程序包安装就好了,安装完成后添加系统环境变量这个就不用多说了,最后就 sdcc -v 测试检验。就这么简单。。。

2、 MinGW-w64

下载地址:https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/

安装完成后需要添加系统环境变量,可以利用 cmd 命令: gcc -v 测试。

3、 MSYS2 或者 Git(只要支持 shell 命令的终端控制台就行)

msys2 下载 <-- 自戳

Git 自行搜索下载。

2、 VSCode

VSCode 环境部署可以看之前的 STM32 开发之 VS Code + gcc 环境编译 的第三节,然后如果你懂得配置 VSCode 的配置项的话,那么你可以跳过下面的配置操作自己写。

  • c_cpp_properties.json

    {
        "configurations": [
            {
                "name": "C51",
                "includePath": [			// 你的工程中存放 include 的文件夹路径
                    "${workspaceFolder}/**",
                    "${workspaceFolder}/App",
                    "${workspaceFolder}/Libraries/Device/Include",
                    "${workspaceFolder}/Libraries/StdDriver/inc"
                ],
                "defines": [
                    "_DEBUG",
                    "UNICODE",
                    "_UNICODE",
                ],
                "compilerPath": "C:\\Program Files\\SDCC\\bin\\sdcc.exe",	//sdcc bin 路径
                "cStandard": "gnu18",
                "cppStandard": "gnu++14",
                "intelliSenseMode": "gcc-x64"
            }
        ],
        "version": 4
    }
  • tasks.json

    {
        // See https://go.microsoft.com/fwlink/?LinkId=733558
        // for the documentation about the tasks.json format
        "version": "2.0.0",
        "tasks": [
            {
                "label": "Build",
                "type": "shell",
                "command": "make",
                "args": [
                    "target=${fileBasenameNoExtension}"
                ],
                "group": {
                    "kind": "build",
                    "isDefault": true
                }
            }
        ]
    }
  • settings.json

    {
        "files.encoding": "gb2312",
        "files.autoGuessEncoding": true,
        "C_Cpp.errorSquiggles": "Enabled",     // 语法错误
        "files.associations": {
            "main.h": "c",
        },
        
        /* 终端在 Windows 上使用的 shell 的路径 */
        "terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe",
    }

# SDCC 规则(仅对于 MCS51 说明)

1、支持的数据类型

image-20201027235219085

2、存储类型

image-20201027235506151

相对于 Keil,其存储类型关键字加上了前缀 ’ __ ' 双下划线,这也是 SDCC 的特色风格。

__data :这是小内存模式的默认 (通用) 地址空间,声明的变量将被放在 8051 内核的直接寻址 RAM 中。

__idata :这个地址空间中的变量将被分配到 8051 的内部 RAM 的间接可寻址部分。

__pdata :存储类型 pdata 用于访问分页的外部数据存储器。

__xdata :这个地址空间中的变量将被放在外部 RAM 中。

__code :存放程序代码的内存地址空间。

3、存储器模式

SDCC 支持四种存储器模式:(small, medium, large, huge)

采用 SDCC 编译时,默认为小模式。如果要强制 SDCC 使用特定的存储器模式,可使用以下命令行参数(在手册的 P3.3.6 章节可以查询得到):

image-20201029212546249

类似于 Keil 的这个选项(只不过 Keil 是 GUI 操作,SDCC 是命令操作):

image-20201029212824145

关于不同模式下变量的存储位置不一样,可以查阅手册 P3.13 章节;总的来说,对于中(medium)、大(large)、巨大(huge)存储模式来说,所有未指定内部命名地址空间而声明的变量都将分配到外部 RAM 中,这包括所有参数和局部变量(用于不可重入函数),中型模式使用 pdata,大型模式使用 xdata;而小内存模式(small)则默认存放在 data。

4、bit 和 sbit 关键字

bit 和 int、char 之类的差不多,只不过 char = 8 位,bit = 1 位;

sbit 是对应可位寻址空间的一个位。

同样的,在 SDCC 这里加上了前缀 ’ __ ’ 双下划线,变成 __bit__sbit

5、SFR(特殊功能寄存器)

与 bit 关键字类似,表示命名地址空间,用于描述 8051 的特殊函数寄存器和特殊位变量。

eg:

__sfr __at (0x80) P0; /* special function register P0 at location 0x80 */

6、绝对寻址

SDCC 支持采用 __at 关键字表示绝对寻址。

7、内嵌汇编

SDCC 完全支持内嵌汇编。使用该功能时,汇编代码应嵌在 __asm__endasm 标识符之间。

8、编译生成文件

  • xxx.asm:程序的汇编文件。
  • xxx.lst:程序的列表文件。
  • xxx.rst:被链接器更新的列表文件。
  • xxx.sym:由链接器生成的符号清单。
  • xxx.rel:由汇编器生成的对象文件,提供给链接器使用。
  • xxx.map:被链接器更新的最终存储器映射。
  • xxx.mem:内存的使用情况摘要。
  • xxx.ihx:Intel 十六进制格式的加载模块。该文件必须被下载到微控制器中。

# SDCC 头文件处理

前面说了,sdcc 的非标关键字是带 ‘ __ ' 双下滑线的,但是 MS51 的官方 SDK 包中,寄存器寻址的关键字全是 keil 格式的,这就需要转换过来;如果是一些成熟的 8051 内核单片机,那么你可以在 sdcc 的安装路径 ...\SDCC\include\mcs51 下找到对应的芯片头文件,若是没有,那么就要自己进行格式转换了。sdcc 格式转换,你可以去上网搜一下,这里给一个链接:https://www.amobbs.com/thread-5625040-1-1.html,里面有提供一个转换工具,当然你也可以自己去写一个程序。

MS51 include

# 工程构建

因为是用 VSCode 做编辑开发,只要有 .vscode 文件夹的配置项就可以了,剩下的编译过程就交给 sdcc 编译,所以工程的构建比较简单,文件夹创建以及移植 SDK 库都方便,以下是我的工程文件分布(看起来还是比较容易理解的):

image-20201028213341368

这里 Libraries 文件夹的内容是直接移植 SDK 库的,其余的看文件名就知道用途了。

另外就是,App 文件夹里,除了 lint.h(用来语法解析 mcs51 特定代码)和添加了 main.h 头文件(往常 main 主文件是不带头文件);然后为什么要这两个呢,是为了避免 VSCode 的语法错误的,当然你也可以一劳永逸,直接关了 VSCode 的语法提示(这个可以看上面的 settings.json 配置文件),至于 lint.h 是从 sdcc 的安装路径 ...\SDCC\include\mcs51 提取出来的,原滋原味。

# VSCode 语法修饰

上面也讲了,sdcc 使用了部分非 ASCII C 关键字,所以 VSCode 会在程序中凸显语法错误;那么,我们就来解决这个问题(当然,不是用关闭语法检查这种粗暴形式):

1、首先要了解的是,在使用 sdcc 进行编译的时候,是会自动在进行编译前预定义 __SDCC 宏的,这样就好办,利用条件编译,区别智能提示运行环境和 SDCC 实际编译环境,用空的 define 去取代这些关键字,寄存器也都用宏代替,然后在 SDCC 实际编译时调用原来 C51 语法的寄存器定义。

2、根据上面第一点,然后结合上面的提到的 lint.h(默认是留了 sdcc 关键字的空 define),得到这样的一个例子:

#ifdef __SDCC
    __sfr __at (0x80) P0;    // 实际有效的寄存器定义
#else
	/* 关键字部分 */
	#define __sfr                    // 空的关键字宏,消除关键字不兼容 (在 lint.h 上可以获取到相关的关键字)
	...
	...
	/* 寄存器部分 */
	#define P0    (*(char *) (0x80)) // 无实际意义,用于兼容(欺骗)标准 C 语法的寄存器符号
	...
	...
#endif

通过以上条件编译,就可以把代码区分到智能提示和实际编译两个环境:

  • 在实际编译时,SDCC 编译器会预定义 __SDCC 宏,因此实际编译时使用实际有效的寄存器定义;
  • 而在智能提示环境,用空的宏取代所有关键字,消除关键字的不兼容,然后用一个宏定义寄存器,保证寄存器名智能提示依然可以使用。这里将寄存器定义为 char* 指针解引用的左值表达式,目的是为迎合语法上对寄存器赋值是合法的,括号里的值可以是任意值,意义不大,当然如果使用寄存器本来的值更合适,但处理起来比较麻烦。

3、对上面的 1、2 点总结起来,就可以得到:

#ifndef __MAIN_H__
#define __MAIN_H__
#include <stdint.h>
#ifdef __SDCC
    #include "MS51_16K.h"
#else
    #include <stdbool.h>
    #include "lint.h"
    #include "SFR_Macro_MS51_16K.h"
/******************************************************************************/
/*                      Macro define  header files                            */
/******************************************************************************/
#define P0          (*(char *) (0)) //= 0x80;
#define SP          (*(char *) (0)) //= 0x81;
#define DPL         (*(char *) (0)) //= 0x82;
#define DPH         (*(char *) (0)) //= 0x83;
#define RCTRIM0     (*(char *) (0)) //= 0x84;
#define RCTRIM1     (*(char *) (0)) //= 0x85;  
#define RWK         (*(char *) (0)) //= 0x86;
#define PCON        (*(char *) (0)) //= 0x87;
#define TCON        (*(char *) (0)) //= 0x88;
#define TMOD        (*(char *) (0)) //= 0x89;
#define TL0         (*(char *) (0)) //= 0x8A;
#define TL1         (*(char *) (0)) //= 0x8B;
#define TH0         (*(char *) (0)) //= 0x8C;
#define TH1         (*(char *) (0)) //= 0x8D;
#define CKCON       (*(char *) (0)) //= 0x8E;
#define WKCON       (*(char *) (0)) //= 0x8F;
#define P1          (*(char *) (0)) //= 0x90;
#define SFRS        (*(char *) (0)) //= 0x91; //TA Protection
#define CAPCON0     (*(char *) (0)) //= 0x92;
#define CAPCON1     (*(char *) (0)) //= 0x93;
#define CAPCON2     (*(char *) (0)) //= 0x94;
#define CKDIV       (*(char *) (0)) //= 0x95;
#define CKSWT       (*(char *) (0)) //= 0x96; //TA Protection
#define CKEN        (*(char *) (0)) //= 0x97; //TA Protection
#define SCON        (*(char *) (0)) //= 0x98;
#define SBUF        (*(char *) (0)) //= 0x99;
#define SBUF_1      (*(char *) (0)) //= 0x9A;
#define EIE         (*(char *) (0)) //= 0x9B;
#define EIE1        (*(char *) (0)) //= 0x9C;
#define CHPCON      (*(char *) (0)) //= 0x9F; //TA Protection
#define P2          (*(char *) (0)) //= 0xA0;
#define AUXR1       (*(char *) (0)) //= 0xA2;
#define BODCON0     (*(char *) (0)) //= 0xA3; //TA Protection
#define IAPTRG      (*(char *) (0)) //= 0xA4; //TA Protection
#define IAPUEN      (*(char *) (0)) //= 0xA5;  //TA Protection
#define IAPAL       (*(char *) (0)) //= 0xA6;
#define IAPAH       (*(char *) (0)) //= 0xA7;
#define IE          (*(char *) (0)) //= 0xA8;
#define SADDR       (*(char *) (0)) //= 0xA9;
#define WDCON       (*(char *) (0)) //= 0xAA; //TA Protection
#define BODCON1     (*(char *) (0)) //= 0xAB; //TA Protection
#define P3M1        (*(char *) (0)) //= 0xAC;
#define P3S         (*(char *) (0)) //= 0xAC; //Page1
#define P3M2        (*(char *) (0)) //= 0xAD;
#define P3SR        (*(char *) (0)) //= 0xAD; //Page1
#define IAPFD       (*(char *) (0)) //= 0xAE;
#define IAPCN       (*(char *) (0)) //= 0xAF;
#define P3          (*(char *) (0)) //= 0xB0;
#define P0M1        (*(char *) (0)) //= 0xB1;
#define P0S         (*(char *) (0)) //= 0xB1; //Page1
#define P0M2        (*(char *) (0)) //= 0xB2;
#define P0SR        (*(char *) (0)) //= 0xB2; //Page1
#define P1M1        (*(char *) (0)) //= 0xB3;
#define P1S         (*(char *) (0)) //= 0xB3; //Page1
#define P1M2        (*(char *) (0)) //= 0xB4;
#define P1SR        (*(char *) (0)) //= 0xB4; //Page1
#define P2S         (*(char *) (0)) //= 0xB5; 
#define IPH         (*(char *) (0)) //= 0xB7;
#define PWMINTC     (*(char *) (0)) //= 0xB7;  //Page1
#define IP          (*(char *) (0)) //= 0xB8;
#define SADEN       (*(char *) (0)) //= 0xB9;
#define SADEN_1     (*(char *) (0)) //= 0xBA;
#define SADDR_1     (*(char *) (0)) //= 0xBB;
#define I2DAT       (*(char *) (0)) //= 0xBC;
#define I2STAT      (*(char *) (0)) //= 0xBD;
#define I2CLK       (*(char *) (0)) //= 0xBE;
#define I2TOC       (*(char *) (0)) //= 0xBF;
#define I2CON       (*(char *) (0)) //= 0xC0;
#define I2ADDR      (*(char *) (0)) //= 0xC1;
#define ADCRL       (*(char *) (0)) //= 0xC2;
#define ADCRH       (*(char *) (0)) //= 0xC3;
#define T3CON       (*(char *) (0)) //= 0xC4;
#define PWM4H       (*(char *) (0)) //= 0xC4; //Page1
#define RL3         (*(char *) (0)) //= 0xC5;
#define PWM5H       (*(char *) (0)) //= 0xC5;  //Page1
#define RH3         (*(char *) (0)) //= 0xC6;
#define PIOCON1     (*(char *) (0)) //= 0xC6; //Page1
#define TA          (*(char *) (0)) //= 0xC7;
#define T2CON       (*(char *) (0)) //= 0xC8;
#define T2MOD       (*(char *) (0)) //= 0xC9;
#define RCMP2L      (*(char *) (0)) //= 0xCA;
#define RCMP2H      (*(char *) (0)) //= 0xCB;
#define TL2         (*(char *) (0)) //= 0xCC; 
#define PWM4L       (*(char *) (0)) //= 0xCC; //Page1
#define TH2         (*(char *) (0)) //= 0xCD;
#define PWM5L       (*(char *) (0)) //= 0xCD; //Page1
#define ADCMPL      (*(char *) (0)) //= 0xCE;
#define ADCMPH      (*(char *) (0)) //= 0xCF;
#define PSW         (*(char *) (0)) //= 0xD0;
#define PWMPH       (*(char *) (0)) //= 0xD1;
#define PWM0H       (*(char *) (0)) //= 0xD2;
#define PWM1H       (*(char *) (0)) //= 0xD3;
#define PWM2H       (*(char *) (0)) //= 0xD4;
#define PWM3H       (*(char *) (0)) //= 0xD5;
#define PNP         (*(char *) (0)) //= 0xD6;
#define FBD         (*(char *) (0)) //= 0xD7;
#define PWMCON0     (*(char *) (0)) //= 0xD8;
#define PWMPL       (*(char *) (0)) //= 0xD9;
#define PWM0L       (*(char *) (0)) //= 0xDA;
#define PWM1L       (*(char *) (0)) //= 0xDB;
#define PWM2L       (*(char *) (0)) //= 0xDC;
#define PWM3L       (*(char *) (0)) //= 0xDD;
#define PIOCON0     (*(char *) (0)) //= 0xDE;
#define PWMCON1     (*(char *) (0)) //= 0xDF;
#define ACC         (*(char *) (0)) //= 0xE0;
#define ADCCON1     (*(char *) (0)) //= 0xE1;
#define ADCCON2     (*(char *) (0)) //= 0xE2;
#define ADCDLY      (*(char *) (0)) //= 0xE3;
#define C0L         (*(char *) (0)) //= 0xE4;
#define C0H         (*(char *) (0)) //= 0xE5;
#define C1L         (*(char *) (0)) //= 0xE6;
#define C1H         (*(char *) (0)) //= 0xE7;
#define ADCCON0     (*(char *) (0)) //= 0xE8;
#define PICON       (*(char *) (0)) //= 0xE9;
#define PINEN       (*(char *) (0)) //= 0xEA;
#define PIPEN       (*(char *) (0)) //= 0xEB;
#define PIF         (*(char *) (0)) //= 0xEC;
#define C2L         (*(char *) (0)) //= 0xED;
#define C2H         (*(char *) (0)) //= 0xEE;
#define EIP         (*(char *) (0)) //= 0xEF;
#define B           (*(char *) (0)) //= 0xF0;
#define CAPCON3     (*(char *) (0)) //= 0xF1;
#define CAPCON4     (*(char *) (0)) //= 0xF2;
#define SPCR        (*(char *) (0)) //= 0xF3;
#define SPCR2       (*(char *) (0)) //= 0xF3; //Page1
#define SPSR        (*(char *) (0)) //= 0xF4;
#define SPDR        (*(char *) (0)) //= 0xF5;
#define AINDIDS     (*(char *) (0)) //= 0xF6;
#define EIPH        (*(char *) (0)) //= 0xF7;
#define SCON_1      (*(char *) (0)) //= 0xF8;
#define PDTEN       (*(char *) (0)) //= 0xF9; //TA Protection
#define PDTCNT      (*(char *) (0)) //= 0xFA; //TA Protection
#define PMEN        (*(char *) (0)) //= 0xFB;
#define PMD         (*(char *) (0)) //= 0xFC;
#define EIP1        (*(char *) (0)) //= 0xFE;
#define EIPH1       (*(char *) (0)) //= 0xFF;
/*  BIT Registers  */
/*  SCON_1  */
#define SM0_1      (*(char *) (0)) //= SCON_1^7;
#define FE_1       (*(char *) (0)) //= SCON_1^7; 
#define SM1_1      (*(char *) (0)) //= SCON_1^6; 
#define SM2_1      (*(char *) (0)) //= SCON_1^5; 
#define REN_1      (*(char *) (0)) //= SCON_1^4; 
#define TB8_1      (*(char *) (0)) //= SCON_1^3; 
#define RB8_1      (*(char *) (0)) //= SCON_1^2; 
#define TI_1       (*(char *) (0)) //= SCON_1^1; 
#define RI_1       (*(char *) (0)) //= SCON_1^0; 
/*  ADCCON0  */
#define ADCF       (*(char *) (0)) //= ADCCON0^7;
#define ADCS       (*(char *) (0)) //= ADCCON0^6;
#define ETGSEL1    (*(char *) (0)) //= ADCCON0^5;
#define ETGSEL0    (*(char *) (0)) //= ADCCON0^4;
#define ADCHS3     (*(char *) (0)) //= ADCCON0^3;
#define ADCHS2     (*(char *) (0)) //= ADCCON0^2;
#define ADCHS1     (*(char *) (0)) //= ADCCON0^1;
#define ADCHS0     (*(char *) (0)) //= ADCCON0^0;
/*  PWMCON0  */
#define PWMRUN     (*(char *) (0)) //= PWMCON0^7;
#define LOAD       (*(char *) (0)) //= PWMCON0^6;
#define PWMF       (*(char *) (0)) //= PWMCON0^5;
#define CLRPWM     (*(char *) (0)) //= PWMCON0^4;
/*  PSW */
#define CY         (*(char *) (0)) //= PSW^7;
#define AC         (*(char *) (0)) //= PSW^6;
#define F0         (*(char *) (0)) //= PSW^5;
#define RS1        (*(char *) (0)) //= PSW^4;
#define RS0        (*(char *) (0)) //= PSW^3;
#define OV         (*(char *) (0)) //= PSW^2;
#define P          (*(char *) (0)) //= PSW^0;
/*  T2CON  */
#define TF2        (*(char *) (0)) //= T2CON^7;
#define TR2        (*(char *) (0)) //= T2CON^2;
#define CM_RL2     (*(char *) (0)) //= T2CON^0;
 
/*  I2CON  */
#define I2CEN      (*(char *) (0)) //= I2CON^6;
#define STA        (*(char *) (0)) //= I2CON^5;
#define STO        (*(char *) (0)) //= I2CON^4;
#define SI         (*(char *) (0)) //= I2CON^3;
#define AA         (*(char *) (0)) //= I2CON^2;
#define I2CPX  	(*(char *) (0)) //= I2CON^0;
/*  IP  */  
#define PADC       (*(char *) (0)) //= IP^6;
#define PBOD       (*(char *) (0)) //= IP^5;
#define PS         (*(char *) (0)) //= IP^4;
#define PT1        (*(char *) (0)) //= IP^3;
#define PX1        (*(char *) (0)) //= IP^2;
#define PT0        (*(char *) (0)) //= IP^1;
#define PX0        (*(char *) (0)) //= IP^0;
/*  P3  */  
#define P30    	(*(char *) (0)) //= P3^0;
/*  IE  */
#define EA         (*(char *) (0)) //= IE^7;
#define EADC       (*(char *) (0)) //= IE^6;
#define EBOD       (*(char *) (0)) //= IE^5;
#define ES         (*(char *) (0)) //= IE^4;
#define ET1        (*(char *) (0)) //= IE^3;
#define EX1        (*(char *) (0)) //= IE^2;
#define ET0        (*(char *) (0)) //= IE^1;
#define EX0        (*(char *) (0)) //= IE^0;
/*  P2  */ 
#define P20        (*(char *) (0)) //= P2^0;
/*  SCON  */
#define SM0        (*(char *) (0)) //= SCON^7;
#define FE         (*(char *) (0)) //= SCON^7; 
#define SM1        (*(char *) (0)) //= SCON^6; 
#define SM2        (*(char *) (0)) //= SCON^5; 
#define REN        (*(char *) (0)) //= SCON^4; 
#define TB8        (*(char *) (0)) //= SCON^3; 
#define RB8        (*(char *) (0)) //= SCON^2; 
#define TI         (*(char *) (0)) //= SCON^1; 
#define RI         (*(char *) (0)) //= SCON^0; 
/*  P1  */     
#define P17  		(*(char *) (0)) //= P1^7;
#define P16  		(*(char *) (0)) //= P1^6;
#define TXD_1  	(*(char *) (0)) //= P1^6; 
#define P15  		(*(char *) (0)) //= P1^5;
#define P14  		(*(char *) (0)) //= P1^4;
#define SDA  		(*(char *) (0)) //= P1^4;    
#define P13  		(*(char *) (0)) //= P1^3;
#define SCL  		(*(char *) (0)) //= P1^3;  
#define P12        (*(char *) (0)) //= P1^2; 
#define P11        (*(char *) (0)) //= P1^1;
#define P10        (*(char *) (0)) //= P1^0;
/*  TCON  */
#define TF1        (*(char *) (0)) //= TCON^7;
#define TR1        (*(char *) (0)) //= TCON^6;
#define TF0        (*(char *) (0)) //= TCON^5;
#define TR0        (*(char *) (0)) //= TCON^4;
#define IE1        (*(char *) (0)) //= TCON^3;
#define IT1        (*(char *) (0)) //= TCON^2;
#define IE0        (*(char *) (0)) //= TCON^1;
#define IT0        (*(char *) (0)) //= TCON^0;
/*  P0  */  
#define P07        (*(char *) (0)) //= P0^7;
#define RXD        (*(char *) (0)) //= P0^7;
#define P06        (*(char *) (0)) //= P0^6;
#define TXD        (*(char *) (0)) //= P0^6;
#define P05        (*(char *) (0)) //= P0^5;
#define P04        (*(char *) (0)) //= P0^4;
#define STADC      (*(char *) (0)) //= P0^4;
#define P03        (*(char *) (0)) //= P0^3;
#define P02        (*(char *) (0)) //= P0^2;
#define RXD_1      (*(char *) (0)) //= P0^2;
#define P01        (*(char *) (0)) //= P0^1;
#define MISO       (*(char *) (0)) //= P0^1;
#define P00        (*(char *) (0)) //= P0^0;
#define MOSI       (*(char *) (0)) //= P0^0;
#endif /* __SDCC */
#endif /* __MAIN_H__ */

对于寄存器定义处理,可以直接 copy 原来的 include 文件内容,然后直接把 sfrsbit 替换成 #define ,再把 = 替换成 (*(char *) (0)) //= 这样就好,是不是很 nice。

# Makefile 程序化管理

SDCC 并不支持同时编译多个源代码文件,所以多文件项目的编译需要分步进行。假如你的项目包含 foo1.c foo2.c main.c 三个文件,那么编译过程如下:

sdcc -c foo1.c
sdcc -c foo2.c
sdcc main.c foo1.rel foo2.rel

还可以使用以下方式编译:

sdcc -c main.c
sdcc main.rel foo1.rel foo2.rel

值得一提的是,sdcc 与 gcc 的命令支持还是有点出入的,但大部分都兼容,因此具体支持哪些命令,需要去翻看 sdcc 的手册。

对于多文件项目最好是写一个 Makefile 文件来维护或者写一个 bat 批处理文件。这里就直接给出我所用的 Makefile 文件吧,分析什么的,可以看以前的链接:https://blog.csdn.net/qq_42992084/article/details/95893283;如果你是 Linux 用户,应该很清楚这些命令,若果诸位大佬有懂得多的,还请在评论区不吝赐教:

######################################
# target path
######################################
TARGET = MS51FB
#######################################
# Build path
#######################################
BUILD_DIR = build
######################################
# source
######################################
SRCDIR = App
LIB_SRC = #Libraries/StdDriver/src
USER_SRC = source#/bsp.c \
source/bsp_time.c \
source/bsp_uart.c
# C sources
C_SOURCES := $(wildcard $(SRCDIR)/*.c $(LIB_SRC)/*.c)
C_SOURCES += $(wildcard $(USER_SRC)/*.c)
ASM_SOURCES = $(wildcard $(SRCDIR)/*.asm)
C_SRC_FILE = $(notdir $(C_SOURCES))
C_OBJ_FILE = $(C_SRC_FILE:%.c=%.c.rel)
ASM_SRC_FILE = $(notdir $(ASM_SOURCES))
ASM_OBJ_FILE = $(ASM_SRC_FILE:%.asm=%.asm.rel)
######################################
# building variables
######################################
# debug build?
DEBUG = 1
# optimization
OPT = 
#######################################
# cross compile
#######################################
PREFIX = 
CC = $(PREFIX)sdcc 
AS = $(PREFIX)sdas8051
MCU_MODEL = -mmcs51
RM = -rm -rf 
MAKE = make 
# ------------------------------------------------------
# Usually SDCC's small memory model is the best choice.  If
# you run out of internal RAM, you will need to declare
# variables as "xdata", or switch to larger model
# Memory Model (small, medium, large, huge)
MODEL  = --model-small
# ------------------------------------------------------
# Memory Layout
# PRG Size = 4K Bytes
#CODE_SIZE = --code-loc 0x0000 --code-size 18432
CODE_SIZE = --code-size 18432
# INT-MEM Size = 256 Bytes
#IRAM_SIZE = --idata-loc 0x0000  --iram-size 256
IRAM_SIZE = --iram-size 256
# EXT-MEM Size = 4K Bytes
#XRAM_SIZE = --xram-loc 0x0000 --xram-size 768
XRAM_SIZE = --xram-size 768
# ------------------------------------------------------
#######################################
# FLAGS
#######################################
# macros for gcc
# AS defines
AS_DEFS = 
# C defines
C_DEFS = 
# AS includes
AS_INCLUDES = 
# C includes
C_INCLUDES =  \
-IApp \
-ILibraries/Device/Include \
-ILibraries/StdDriver/inc \
-Iinclude
# libraries
LIBS = 
LIBDIR = 
# compile gcc flags
ASFLAGS = -l -s
CFLAGS = $(MCU_MODEL) $(C_DEFS) $(C_INCLUDES) $(MODEL) --out-fmt-ihx --no-xinit-opt --peep-file tools/peep.def
ifeq ($(DEBUG), 1)
CFLAGS += 
else
CFLAGS += $(OPT)
endif
#######################################
# LDFLAGS
#######################################
LDFLAGS = $(LIBDIR) $(LIBS) $(MCU_MODEL) $(MODEL) $(CODE_SIZE) $(IRAM_SIZE) $(XRAM_SIZE) --out-fmt-ihx
# default action: build all
.PHONY: all
all: $(BUILD_DIR)/$(TARGET).hex
#######################################
# build the application
#######################################
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(C_OBJ_FILE))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(ASM_OBJ_FILE))
$(BUILD_DIR)/%.c.rel: $(USER_SRC)/%.c
	$(CC) -o $@ $(CFLAGS) -c $^
$(BUILD_DIR)/%.c.rel: $(LIB_SRC)/%.c
	$(CC) -o $@ $(CFLAGS) -c $^
$(BUILD_DIR)/%.c.rel: $(SRCDIR)/%.c
	$(CC) -o $@ $(CFLAGS) -c $^
$(BUILD_DIR)/%.asm.rel: $(SRCDIR)/%.asm
	$(AS) $(ASFLAGS) -o $@ $^ 
$(BUILD_DIR)/%.ihx: $(OBJECTS)
	$(CC) -o $@ $(LDFLAGS) $^
$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.ihx | $(BUILD_DIR)
	packihx $^ > $@
$(BUILD_DIR):
	mkdir $@
#######################################
# clean up
#######################################
.PHONY: clean
clean:
	$(RM)$(BUILD_DIR)/*
#######################################
# build asm
#######################################
HEADER_FILE = MS51_16K.h
HEADER_PATH = App
disasm: $(BUILD_DIR)/$(TARGET).hex
	./tools/mcs51-disasm.pl -M $(HEADER_FILE) -I $(HEADER_PATH) -fl -rj -as $(BUILD_DIR)/$(TARGET).hex > $(BUILD_DIR)/$(TARGET).a51
# *** EOF ***

这里说一下,sdcc 特有的 packihx 命令是用来产生 Intel HEX 文件的; mkdir 命令在 sdcc 中是不支持,可以把他删掉,由于这一点,所以得保留着 build 文件夹存放编译文件,如果删除的话,执行会出错;执行 disasm 命令需要工具链 mcs51-disasm.pl 的支持,它的说明如下:

image-20201028231938212;另外, += 好像也不支持单文件添加,看 USER_SRC 处,只能通过 wildcard 扫描添加。。。不知为啥,望大佬解答一二。

# 程序编译

#include "main.h"
#include "bsp.h"
#include "bsp_uart.h"
#include "bsp_time.h"
#include "bsp_eeprom.h"
#include "bsp_adc.h"
#include "bsp_pwm.h"
#include "bsp_wdt.h"
__bit BIT_TMP;			//EA 暂存(对应官方库)
#define ENABLE_WDT				1
/* ISR 中断函数原型声明(原因看手册 P3.8 章节) */
void UART0_ISR(void) __interrupt (4);
void Timer3_ISR(void) __interrupt (16);
/************************************************
函数名称 : System_Start
功    能 : 系统初始化
参    数 : 无
返 回 值 : 无
*************************************************/
void System_Start(void)
{
    clr_EA;
    Bsp_Init();
    UART0_Timer1_Init();
    Timer3_Init(TIME_DIV16, 15000);  // 10ms
	// Timer0_Init();
	// ADC_Config();
	// PWM0_Init();
#if ENABLE_WDT
    WDT_Init();
#endif /* ENABLE_WDT */
    set_EA;
}
/************************************************
函数名称 : main
功    能 : 主函数入口
参    数 : 无
返 回 值 : int
*************************************************/
int main(void)
{
    uint16_t i = 0;
    System_Start();
	P12_QUASI_MODE;
    P12 = 1;
    for(i=0; i<3; i++)
    {
        P12 ^= 1;
        SoftwareDelay_ms(0xFF);
    }
#if ENABLE_WDT
	WDT_EnableOpen();
	
#else
	WDT_DisableClose();
	
#endif /* ENABLE_WDT */
	
    while(1)
    {
#if ENABLE_WDT
        WDT_ReloadCounter();
#endif /* ENABLE_WDT */
        printf_small("\n Hello world!");
        SoftwareDelay_ms(0xFF);
    }
}
/*---------------------------- END OF FILE ----------------------------*/

然后 make 编译,最终输出(方框处显示成功):

image-20201028233905135

下载进去,就可以看到 hello world 在不停的打印输出了。

在这里,需要注意以下几点:

1、中断函数必须在 main 函数文件中给出 ISR 原型,不然就无法进中断。详细请看手册的 P3.8 章节。以下摘自部分解释:

image-20201028234927141

2、一般,我们在 C 程序中打印输出是调用 printf 语句进行输出的,但在 sdcc 上,比较建议使用 printf_small 输出,因为对于 8 位微控制起来说,资源是很紧缺的,使用 printf_small 已经可以满足一般输出需求了,当然以上仅限于输出整型以及字符型变量;对于浮点型变量,需要使用特殊的指令对程序进行编译才能得到输出效果,具体的介绍可以看手册的 P3.14.1 章节。

3、如果是使用 bin 文件烧写到芯片上,可以用 sdcc 自带的 makebin.exe 命令行工具进行转换(不过这个转换出来文件比较大),路径可以在 ...\SDCC\bin 下找到,通过以下命令: makebin xxx.ihx > xxx.bin

或者利用 hex2bin,下载地址:https://sourceforge.net/projects/hex2bin/files/latest/download,这个的命令则是: hex2bin xxx.hex > xxx.bin

Makefile 下的 bin 文件生成命令:

image-20201029233621896

两者相比之下,由于前者是做了剩余空间填充处理的,所以转换出来的文件比较大,个人更倾向于后者。

# 总结

1、不能使用 double 数据类型,否则报错。
2、make 编译只能根据法则编译对应文件夹的全部源文件,不能选择编译相应源文件。
3、中断函数这里是要在 main 函数所在文件处进行原型声明,否者是无法进入中断程序,原因不声明是并没用把中断函数的向量地址加载到执行文件中。
4、sdcc 使用的关键字是跟 keilC51 里面的关键字不同的;对于一些非 ANSI C 的关键字,SDCC 均采用双下滑线开头的方式定义,具体可看 sdcc 手册。
5、sdcc 支持的命令行命令,跟我们平常用的 gcc 命令行命令有所不同,具体翻看 sdcc 手册。
6、一般串口重定向后,是使用 printf 函数输出,但在 sdcc 编译器中要改用 printf_small 这个函数进行替代。

7、sdcc 在编译文件时,会把用不到的代码也编译进来,所以如果空间紧张,建议注释掉一些无关的代码,避免代码空间膨胀。

# 相关链接

SDCC Compiler User Guide

8051 C Development Using SDCC(Small Device C Compiler)

Nuvoton N76E003 with SDCC

新唐 N76E003 8051 1T 单片机入坑记录

SDCC 编译器简明使用教程

使用免费的 SDCC C 编译器开发 DS89C430/450 系列微控制器固件

51 单片机之开发环境使用 VSCode 结合 SDCC 取代 Keil

使用 Visual Studio Code + CMake + SDCC 进行 C51 开发的一次尝试

台湾同胞对 SDCC 的使用介绍

sdcc man 阅读笔记

SDCC printf 函數介紹