本帖最后由 lcw_swust 于 2013-6-1 11:13 编辑
本程序由一个解释器修改而来,由于需要较大内存,目前只是在KEIL下软件仿真运行
上程序:
TC编译器V1.1.rar
(158.74 KB, 下载次数: 344)
2013-6-1 11:12 上传
点击文件名下载附件
/*--------------------------------------------------
TC编译器说明
目标:
单片机中运行编译器,能编译简单的C语言程序,生成汇编
代码,再将汇编代码转为虚拟机的机器码,再让虚拟机执行机器码。
关于简单的C语言程序:
实现if,while,一维数组,API函数调用。
数据类型 (signed/unsigned)charintlong (*)
--------------------------------------------------
虚拟机结构:
寄存器(全32位):r0~rf(共16个)
内存: 自定义
寄存器使用规则:
函数入口用r0、r1、r2、r3传递
函数返回用r4,计算结果也用r4
计算使用寄存器r4、r5、r6、r7
Rc用于进入函数时保存Re,便于计算局部变量的偏移量
Rd用于指针查表(相当于x指针,用于ld、st指令)
Re用于申请变量空间(相当于y指针)
Rf用于堆栈(相当于sp,用于push、pop、call、ret指令)
其余寄存器备用
虚拟机指令集:
算术指令:
(全是寄存器作为操作数)
neg(~),not(!),add(+),sub(-),mul(*),div(/),mod(%)
and(&),or(|),xor(^),grt(>),les(<),sl(<<),sr(>>)
land(&&),lor(||),nless(>=),ngrt(<=),neq(!=),equ(==)
跳转指令:
ret,jmp,call,
je (r4=0则跳转)
jne (r4!=0则跳转)
数据传送:
mov r,r (寄存器复制)
ldi r,i (加载立即数)
//ld系列
ldu8 r (r=*(U8*)rd,从RAM或ROM加载无符号8位数)
lds8 r (r=*(S8*)rd,从RAM或ROM加载有符号8位数)
ldu16 r (r=*(U16*)rd,从RAM或ROM加载无符号16位数)
lds16 r (r=*(S16*)rd,从RAM或ROM加载有符号16位数)
ldu32 r (r=*(U32*)rd,从RAM或ROM加载无符号32位数)
lds32 r (r=*(S32*)rd,从RAM或ROM加载有符号32位数)
//st系列
stu8 r (*(U8*)rd=r,向内存写入无符号8位数)
sts8 r (*(S8*)rd=r,向内存写入有符号8位数)
stu16 r (*(U16*)rd=r,向内存写入无符号16位数)
sts16 r (*(S16*)rd=r,向内存写入有符号16位数)
stu32 r (*(U32*)rd=r,向内存写入无符号32位数)
sts32 r (*(S32*)rd=r,向内存写入有符号32位数)
push r (sp-=4,r=*sp,sp即rf)
pop r (r=*sp,sp+=4,)
--------------------------------------------------
C程序转为汇编代码的过程:
(参考TCCMP.C文件)
1.编译中使用的缓冲区
缓冲区先是被分为独立的3段,普通的代码放入第1段,定义数组时,
数组首地址(标号)赋给第1段中的变量,数组元素的值放入第2段,
如果数组元素是双引号字符串,则将双引号字符串首地址(标号)
放入第2段,字符串放入第3段。
这些参考AddAsmCode1、AddAsmCode2、AddAsmCode3
当然,在写入时有可能出现第1段装不下,写到第2段去的情况,
这时就需要,加大第1段,将第2、3段往右移动(参考moveasm2、moveasm3)。
当然,如果编译器运行环境的缓冲区足够,则可以不用这么麻烦,
直接定义三个缓冲区就是了。
最后,将3段连接起来变一个字符串,用于Link
2.变量定义
(参考addvar函数)
假如一个变量占两字节,则只需让re增加2:
ldi r4,2
add re,r4 //re=re+2
3.读取变量
(参考getvar_id函数)
mov rd,rc //rc=rd=进入函数时保存的局部变量的RAM区首地址
ldi r4,2 //r4=该局部变量在局部变量缓冲区中的偏移地址
add rd,r4 //rd=该变量的绝对地址
ldu8 r4 //r4=*(U8*)rd,读到该变量的值,根据变量的类型,ldu8可能为lds8ldu16lds16等
4.写变量
(参考setvar函数)
mov rd,rc
ldi r5,2
add rd,r5 //rd=该变量的绝对地址
stu8 r4 //*(U8*)rd=r4
5.数组
(参考vardef1内state==10的处理)
定义数组赋初值时,则生成一个标号,标号的值赋给该数组变量,然后将数组元素放入另一code段
char s[]="123";
可能被编译成
code第1段----------
......
ldi r4,lab001 //取数组地址
stu32 r4 //变量赋值
code第2段----------
lab001:
arrs8 049,050,051,
long s[]={"123","456"};
可能被编译成
code第1段----------
......
ldi r4,lab001 //取数组地址
sts32 r4 //变量赋值
code第2段----------
lab001:
arru32 lab002,lab003,
code第3段----------
lab002:
arrs8 049,050,051,
lab003:
arrs8 052,053,054,
6.算式
(参考getexp及getexp1)
getexp是将整个算式的值求出,x结果放入r4,
例如 a+b*c
getexp1只是求出算式中的一个参数,并放入r4,就像上面的ac
举个例子,要计算 1+2
调用getexp(),
getexp()内先调用getexp1,
得到代码:
ldi r4,1 //r4=1
getexp()再调用GetToken,取得+号
得到代码:
push r4 //保存r4
调用getexp1得到:
ldi r4,2 //r4=加号右方的2
mov r5,r4 //r5=r4=2
pop r4 //r4=加号左方的1
调用cacu得到:
add r4,r5 //r4=r4+r5=1+2
简单总结:
本编译器将C转成汇编代码基本上是按照固定规律来的,
所以可能会有些冗余,目前未作优化
......
--------------------------------------------------
汇编转为机器码的过程:
(参考Link.C文件)
调用 Link_GetToken,
如果得到的不是以冒号结尾的标号,则当成是指令,
然后查表将Token字符串转为指令码,再根据指令码的
类型获取指令之后的参数,如果参数中有标号,则查找
标号(Link_getcst)。
--------------------------------------------------
机器码的执行过程:
(参考VM.C文件)
这个相对简单,读取一字节的机器码之后,根据指令类
型进行处理,对寄存器、RAM等进行读写,以及调用某些
API函数。
--------------------------------------------------
一些注意事项及BUG:
对于运算式的处理,是按从左到右来的,没有优先级之分
有括号的地方会作为单独的运算式处理
例如
1+2*3 等价于(1+2)*3
1+(2*3) 等价于1+(2*3)
不要在循环内定义参数
函数传递参数的个数以及返回参数没有严格检查
定义变量赋初值时不能取变量值,可以取变量地址
比如int a=&b;是可以的,int a=b;就不行
//--------------------------------------------------*/
程序运行截图
5.jpg (148.92 KB, 下载次数: 1)
下载附件
2013-6-1 11:11 上传
就是在单片机上运行一个编译器,编译C语言。
这样会对单片机资源耗费很多吧,具体有哪些实用价值呢?类似于某些PLC的解释语言吗?你熟悉3S公司的CODESYS软件吗?它似乎是运行的一个什么操作系统,貌似也是解释类型的吧?
也许可以用于某些玩具机器人之类吧,用于练习编程。
也可以像JAVA那样做一些跨平台的东西。
CODESYS不熟。
解释器我也写过,坛里也有开源PLC,不知是不是一样。
https://www.amobbs.com/thread-5492296-1-1.html
一周热门 更多>