干货 | DIY 一个 ARM 学习机
我对底层的东西特别感兴趣。十多年前有了自己的电脑,一直保留DOS用了好些年。喜欢汇编,讨厌Java。接触的第一个单片机是AT89S52,下程序是用串口CTS/RTS以及DSR/DTR控制线来模拟SPI,在DOS下把时钟中断调到几十kHz来定时确定时序。后来就用89S52做成了2051的编程器,再后来用2051做了串口转SPI下载器玩起AVR了,玩过最多的还是AVR.
不过51、AVR这类Harvard结构MCU不能干的事情是自己Load程序来运行,只能从ROM执行啊(51可以有办法映射,但是51学习机计划没有实施)。我小时候听说过“中华学习机”,不同于小霸王学习机那样ROM固化死了的,人家是从软盘装载程序的。家里头的旧杂志上介绍过TD-I型8031的学习机,是用开关来二进制编程RAM然后执行的,倘若放到今天……
我生活的时代已经不是穿孔纸带了,DIY还是玩现代的东西吧。言归正传,我的第一个“学习机”作品是ARM7TDMI的,尽管这个核已经早过时了。我选择了NXP的LPC2220作为处理器,它自带64kB的RAM,在单片机中算是大的了。核心比较简单,作为学习机适宜,而且有外部总线,比较方便扩展RAM,类似PC那样来玩。这个PCB做得比较早了,搁置没玩起来(手头没时间玩的板子多了,这是另外的问题),上面除了MCU还有一块16bit*256k的SRAM,一片8bit*128k的NOR Flash ROM,预留16-bit总线插针扩展,具备一个计算机的配置了(除了缺少DMA支持)。下面是线路图:
PCB的顶层图
PCB的底层:
作为MCU自带的外设是可以引出来的,所以UART, SPI, I2C基本I/O通信用分组接到插针,再另外放了16个GPIO在一侧。核心部分先焊上就可以玩了。
上面介绍的是硬件部分。LPC2220是为数不多的ROMless单片机,里面没有Flash哦,OTP也没有。这也是我选择它的原因,作为学习机不是做任何应用的原型板,烧写Flash尽量避免吧。虽然Flash的烧写次数已经够多了,能省一事算一事。NXP的ARM7 (LPC21xx系列)有个特点是可以从PC直接ISP下载到RAM,然后运行。我在第一次玩ARM (LPC2103)的时候发现Flash工具有这个功能。
读NXP的手册,发现Bootloader是使用串口命令进行交互的,命令也不复杂。
所以,只要连接MCU的UART0到PC的串口,上电或者复位就进入Bootloader里面的ISP程序(因为没有可执行的ROM嘛),就可以从PC把代码直接下载到ROM然后运行了。64kB的SRAM哦,可以实现不少东西了吧。
当然,用NXP官方的FlashMagic来下载的效率太低了,不适应“学习机"要快速载入程序的要求。所以我自己做一个程序:
#include<windows.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
HANDLE dev;
OVERLAPPED oWR = {0}, oRD={0};
void errexit(char *s)
{
fprintf(stderr,"%s",s);
exit(1);
}
struct ROM
{
unsigned char b;
char oc;
};
int readcomm(unsigned char *buf, int maxlen, int timeout);
int writecomm(unsigned char *buf, int len);
int loadhexfile(char *fn,int szlimit,int *poff,struct ROM *rom);
int showbinary(int szlimit, struct ROM *rom);
int uuencode(int szlimit, struct ROM *rom, char *buf, int slen, int linelen);
int checksum(int szlimit, struct ROM *rom);
main(int argc, char *argv[])
{
DCB dcb;
int i,r;
if(argc<2)
{
printf("Supply a HEX file name.\n");
return;
}
dev=CreateFile("COM3", GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING, FILE_FLAG_OVERLAPPED,0);
if(dev==INVALID_HANDLE_VALUE)
{
printf("Cannot open COM3 port.\n");
exit(1);
}
FillMemory(&dcb, sizeof(dcb), 0);
dcb.DCBlength = sizeof(dcb);
BuildCommDCB("57600,n,8,1",&dcb);
if (!SetCommState(dev, &dcb))
{
printf("Cannot set port parameters.\n");
exit(1);
}
oWR.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
oRD.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
for(i=0;i<1000;i++)
{
int len0;
char c, buf[32];
char done=0;
writecomm("?",1);
r=readcomm(buf,4,20);
if(r>0)
{
int j;
for(j=0;j<r;j++)
{
printf("%c",buf[j]);
if(buf[j]=='S')
{
r=readcomm(buf+r,14-(4-j),20);
if(r==14-(4-j) && strncmp(buf+j,"Synchronized\r\n",14)==0)
{
printf("SYNC\n");
writecomm("Synchronized\r\n",14);
r=readcomm(buf,32,50);
if(r==18 && strncmp(buf,"Synchronized\r\nOK\r\n",18)==0)
{
printf("Synchronization done\n");
done=1;
break;
}
else
printf("(%d) %s",r,buf);
}
}
}
if(done) break;
}
else
{
printf(".");
continue;
}
}
{
char c;
writecomm("11095\r\n",7);
while(readcomm(&c,1,10)>0)
{
printf("%c",c);
}
}
{
char c;
writecomm("J\r\n",3);
while(readcomm(&c,1,10)>0)
{
printf("%c",c);
}
}
{
unsigned int progaddr=0x40000200;
char c;
char str[128];
char buf[512];
int slen;
int pgsize;
struct ROM rom[1024];
int offset=-1;
pgsize=loadhexfile(argv[1],1024,&offset,rom);
printf("Program Size: %d bytes from %X\n", pgsize, offset);
showbinary(1024,rom);
sprintf(str,"W %d %d\r\n",progaddr, pgsize);
slen=strlen(str);
writecomm(str,slen);
while(readcomm(&c,1,10)>0)
{
printf("%c",c);
}
slen=uuencode(1024, rom, buf, 512, 61);
writecomm(buf,slen);
sprintf(str,"%d\r\n",checksum(1024,rom));
slen=strlen(str);
writecomm(str,slen);
while(readcomm(&c,1,10)>0)
{
printf("%c",c);
}
writecomm("U 23130\r\n",9);
while(readcomm(&c,1,10)>0)
{
printf("%c",c);
}
sprintf(str,"G %d T\r\n",progaddr);
slen=strlen(str);
writecomm(str,slen);
while(readcomm(&c,1,10)>0)
{
printf("%c",c);
}
}
CloseHandle(oWR.hEvent);
CloseHandle(oRD.hEvent);
CloseHandle(dev);
}
int writecomm(unsigned char *buf, int len)
{
int r;
int len0;
r=WriteFile(dev,buf,len,&len0, &oWR);
if(r==0)
{
if(GetLastError()!=ERROR_IO_PENDING)
{
printf("COM port access error!\n");
exit(1);
}
GetOverlappedResult(dev, &oWR, &len0, TRUE);
}
return len0;
}
int readcomm(unsigned char *buf, int maxlen, int tmout)
{
static unsigned wait=0;
static unsigned char ch;
int i, len0, r;
if(tmout<=0)
return 0;
for(i=0;i<maxlen;i++)
{
if(!wait)
{
if(ReadFile(dev,&ch,1,&len0, &oRD))
{
wait=0;
buf[i]=ch;
continue;
}
if(GetLastError()!=ERROR_IO_PENDING)
{
printf("Read COM port error!\n");
exit(1);
}
}
r=WaitForSingleObject(oRD.hEvent,1);
if(r==WAIT_TIMEOUT)
{
wait=1;
tmout--;
if(tmout==0)
return i;
else
{
i--;
continue;
}
}
if(r==WAIT_OBJECT_0)
{
wait=0;
buf[i]=ch;
continue;
}
}
return i;
}
int loadhexfile(char *fn,int szlimit,int *poff,struct ROM *rom)
{
int i,j,n,count,addr,waddr,bsz;
char line[512];
unsigned char bb,sum,hex[256];
FILE *fp;
memset(rom,0,sizeof(struct ROM)*szlimit);
fp=fopen(fn,"r");
if(fp==NULL)
{
printf("Cannot open HEX file %s\n",fn);
return 0;
}
printf("Opening file %s for reading... ",fn);
count=0;
sum=0;
for(n=1;;n++)
{
if(!fgets(line,512,fp))
errexit("read error\n");
if(line[0]!=':')
errexit("FORMAT ERROR\n");
j=0;
for(i=1;;i+=2) // convert ASCII to binary
{
switch(line[i])
{
case 'A': bb=0xa0; break;
case 'B': bb=0xb0; break;
case 'C': bb=0xc0; break;
case 'D': bb=0xd0; break;
case 'E': bb=0xe0; break;
case 'F': bb=0xf0; break;
case '\0': case '\r': case '\n': bb=0x77; break;
default: if(line[i]>='0' && line[i]<='9')
bb=(line[i]-'0')<<4;
else
bb=0xff;
}
if(bb==0x77)
break;
if(bb==0xff)
errexit("INVALID CHARACTER\n");
switch(line[i+1])
{
case 'A': bb+=0xa; break;
case 'B': bb+=0xb; break;
case 'C': bb+=0xc; break;
case 'D': bb+=0xd; break;
case 'E': bb+=0xe; break;
case 'F': bb+=0xf; break;
default: if(line[i+1]>='0' && line[i+1]<='9')
bb+=(line[i+1]-'0');
else
errexit("BAD BYTE\n");
}
sum+=bb;
hex[j]=bb;
j++;
} // converted ASCII to binary
if(sum!=0)
errexit("CHECKSUM ERROR\n");
if(j<5)
errexit("LINE TOO SHORT\n");
if(hex[0]!=j-5)
errexit("FORMAT ERROR\n");
bsz=j-5;
switch(hex[3])
{
case 0: addr=hex[1]; addr<<=8; addr+=hex[2];
if(*poff==-1)
{
*poff=addr;
}
for(i=0;i<bsz;i++)
{
int waddr=addr-(*poff);
if(waddr>szlimit)
{
printf("address %X beyond range (base %X) \n",addr, *poff);
}
else
{
rom[waddr].b=hex[4+i];
rom[waddr].oc=1;
}
addr++;
count++;
}
break;
case 1: if(j!=5)
errexit("UNEXPECTED END\n");
fclose(fp);
return count;
case 2: if(hex[1]!=0 || hex[2]!=0 || hex[4]!=0 || hex[5]!=0)
printf("UNSUPPORTED PARAGRAPH\n");
break;
case 3:
default: printf("UNSUPPORTED RECODE TYPE (%X)\n", hex[3]); break;
}
}
}
int showbinary(int szlimit, struct ROM *rom)
{
int i;
for(i=0;i<szlimit;)
{
int j;
char blank=1;
for(j=0;j<16;j++)
{
if(rom[i+j].oc)
{
blank=0;
break;
}
}
if(blank)
{
i+=16;
continue;
}
printf("%04X: ",i);
for(j=0;j<16;j++)
{
if(rom[i+j].oc)
printf("%02X",rom[i+j].b);
else
printf("..");
if(j==7)
printf(" - ");
else
printf(" ");
}
printf(" | ");
for(j=0;j<16;j++)
{
if(rom[i+j].oc)
{
if(rom[i+j].b>=0x20 && rom[i+j].b<=0x7f)
printf("%c",rom[i+j].b);
else
printf("?");
}
else
printf(".");
}
i+=16;
printf("\n");
}
}
int uuencode(int szlimit, struct ROM *rom, char *buf, int slen, int linelen)
{
int i, sz=0,mem;
int lmax, lc, brem;
int index=0;
for(i=0;i<szlimit;i++)
{
if(rom[i].oc)
sz=i+1;
}
lmax=((linelen-1)/4)*3;
lc=sz/lmax;
brem=sz%lmax;
mem=lc*(((linelen-1)/4)*4+3)+3+(brem/3*4)+(brem%3+1); // include CR LF
if(mem>=slen)
{
printf("UUEncode: buffer too small! need %d bytes\n",mem);
return 0;
}
for(i=0;i<sz;)
{
int j, len;
char r=0;
unsigned char b, a=0;
if(i+lmax<sz)
len=lmax;
else
len=brem;
buf[index++]=0x20+len;
for(j=0;j<len;j++)
{
if(rom[i+j].oc)
b=rom[i+j].b;
else
b=0xff;
a|=(b>>(2+r));
if(a) buf[index++]=0x20+a; else buf[index++]='`';
r+=2;
a=((b&((1<<r)-1))<<(6-r));
if(r==6)
{
if(a) buf[index++]=0x20+a; else buf[index++]='`';
r=0;
a=0;
}
}
if(r!=0)
{
if(a) buf[index++]=0x20+a; else buf[index++]='`';
}
i+=lmax;
buf[index++]='\r';
buf[index++]='\n';
}
printf("MEM %d, actual %d\n",mem,index);
return index;
}
int checksum(int szlimit, struct ROM *rom)
{
int i, sz, sum;
sz=0;
for(i=0;i<szlimit;i++)
{
if(rom[i].oc)
sz=i+1;
}
sum=0;
for(i=0;i<sz;i++)
sum+=rom[i].b;
return sum;
}
复制代码
这个程序做的工作是打开串口, 和单片机Bootloader ISP命令同步,然后读取指定的HEX文件,下载到LPC2220的RAM地址 0x40000200, 再解除锁定,最后发送执行程序的命令,单片机从 0x40000200 开始运行。如果要重新下载程序,把单片机Reset了,再运行以上的程序即可。命令行一步搞定,容易吧!
注: LPC2220的RAM地址从 0x40000000 开始,但是BootLoader会用一部分RAM,所以为了避免冲突,程序下载从RAM的512字节之后起。NXP的Bootloader是用UUEncode编码传输二进制数据的,上面的程序包含编码的子程序。这个程序也写得不完善,还有比如代码长度检查都没有做进去,目前做到的只是能下载一小段并执行。