开源PLC学习笔记09(再从51开始 主函数)——2013_11_18
本帖最后由 oldbeginner 于 2013-11-18 21:27 编辑理解的主函数框架如下,
void main(void)
{
I0口复位;
RAM复位;
中断复位;
串口初始化;
PLC版本核查;
while(下载模式条件)
{
FX1NProcessing();
if(下载模式条件不满足)
{
软件复位并从用户程序空间启动;
}
}
PLC 代码初次扫描;
while(1)
{
输入输出刷新;
main_PLC();
}
}
****************************************
按照顺序来理解,
一、首先I0口复位,为简单和简洁去除了显示器。
void reset_IO(void) // I,O口初始化
{
P0M0= 0x00;
P0M1= 0xff;
P0 = 0x00;
P1 = 0xff;
P2 = 0xff;
P3 = 0xff;
}
P0M0和P1M1是MPC82G56芯片自己的寄存器,
P0M0:端口0模式寄存器0;
P0M1:端口0模式寄存器1;
P0口采用上拉输出,可以想到,P0对应的输出就是Y。
**************************************
二、 RAM复位,只看X和Y
void reset_RAM(void)
{
for (i=0; i<_Y_BYTE; i++)
{
rY.BYTE = 0;
rY1.BYTE = 0;
}
Timer_5ms = 0; // 5ms时基计数器,5ms Timer0中断计数
Timer_10ms = 0; // 10ms时基计数器,5ms Timer0中断计数
Timer_100ms = 0; // 100ms时基计数器,5ms Timer0中断计数
Pulse_val_Sec = 0; // 1s时基计数器
Pulse_val_Min = 0; // 1min时基计数器
CODE_ERROR = 0;
input_IO();
for (i=0; i<_X_BYTE; i++)
rX1.BYTE = rX.BYTE;
}
因为对rX和rY的结构比较熟悉了,所以容易理解。但是为什么把rX1的赋值放在后面,它前面还有一个input_IO函数,可以想到,input_IO会影响到rX。为了简单,去掉了键盘输入和显示器。
//初始化,输入输出,内存处理 子函数 //
//-------------------------------------------------------------------------------------//
void input_IO(void) // X输入,Y输出刷新
{
unsigned char i;
i = P2;
i = ((i << 1) & 0xaa) | ((i >> 1) & 0x55);
i = ((i << 2) & 0xcc) | ((i >> 2) & 0x33);
rX.BYTE = ~((P1 & 0x0f) | (i & 0xf0));
rX.BYTE = ~(i & 0x0f) & 0x0f;
P0 = rY.BYTE;
}
看了前4行, i 到底是什么?
真巧,4年前的今天。
搜了一下啊,http://wenku.baidu.com/view/fea1e985b9d528ea81c779ee.html,很不幸,目前没有精力顾及。
暂时理解其功能,变换未按照次序输入输出的IO口。 这个PLC加了好多外设,键盘和显示器,如果作为学习那负担承受不起,在学习中,只要不影响核心功能的外设都会被和谐。
继续中断复位,
void reset_interrupt(void)
{
IP = 0x00;
IE = 0x00;
TCON = 0x00; // 定时器控制寄存器, 注意:TCON只需操作一次
TMOD = 0x01;
T0= Value_T0_cons; // 装入5ms Timer0中断常数
PT0 = 1;
ET0 = 1;
TR0 = 1;
}
中断是很常规的那种。
然后是串口复位,串口复位在笔记05中理解过,串口使用了定时器1,而中断复位采用定时器0
void UartInit(void)
{
//停止定时器
TR1=0;
/******电源管理PCON寄存器************
位 7 6 5 4 3 2 1 0
寄存器 SMOD X X X X X X X
————————————————————
取值 1 0 0 0 0 0 0 0
说明 波特率加倍
************************************/
PCON=0x80;
/*****定时器TMOD寄存器**********
位 7 6 5 4 3 2 1 0
寄存器 GATE C/T M1 M0 GATE C/T M1 M0
——————————————---- |———————————
定时器1的设置 定时器0的设置
取值 0 0 1 0 0 0 0 0
说明 使用定时器1,工作方式2 | 没用使用
**********************************/
TMOD=TMOD | 0x20;
//定时器1工作方式2
//在下面的PCON设置上增加了倍频,所以晶振位22.1184Mhz
/**********波特率设置表(22.1184Mhz)************
波特率 2400 4800 9600 19200
TH1 E8 F4 FA FD
TL1 E8 F4 FA FD
说明,因为选取波特率9600,所以TH1和TL1都取FA
*************************************************/
TH1=0xFA;
TL1=0xFA;
//串口工作方式1
/*******串口SCON寄存器***************
位 7 6 5 4 3 2 1 0
寄存器 SM0 SM1 SM2 REN TB8 RB8 TI RI
——————————————————————————————————————
工作方式控制| 多机通信| 接受标志位|发第9位| 收第9位| 发送中断标志位接受中断标志位
取值 0 1 0 1 0 0 0 0
说明 工作方式1 开始接受
**************************************/
SCON=0x50;
//启动定时器1
TR1=1;
UartReceiveCounter=0;
UartRxTimerStartFlag=0;
}
也好理解。
这样,几个复位函数先理解。后面的函数就感觉复杂了。
{:smile:}{:smile:} 不得不顶楼主,你的精研劲头值得大家学习。 kinsno 发表于 2013-11-18 22:28 static/image/common/back.gif
不得不顶楼主,你的精研劲头值得大家学习。
谢谢鼓励,具有精研劲头应该是开源PLC的人,我只不过是来学习一下的,另外写成笔记感觉效率更好些。 oldbeginner 发表于 2013-11-18 22:42 static/image/common/back.gif
谢谢鼓励,具有精研劲头应该是开源PLC的人,我只不过是来学习一下的,另外写成笔记感觉效率更好些。 ...
哈哈,其实你这样也蛮不错,至少能给后面搞PLC的人一个启示或少走一些弯路啊。
其实,PLC现在这些远远不够,只能说先仿仿,最终还是要取其精华,用在我们自己的产品上,或形成我们自己的东西吧;PLC这个行业越来越难啊,西门子最近在成都搞了一个大工厂,S200系列将全部在这里生产,听说现在200已经有大降价了,比以前。 睡不着,上来就碰到楼主的笔记,这段时间虽然还来不及学习,但是先说声感谢! 好东西学习了
建议楼主把笔记做PDF文档,这样文便阅读和学习。。。 继续PLC版本检测,
PLCType=IAPFlashReadMode(PLCTypeAddr); // 上电,核实 PLC 硬件版本号
复习一下地址和函数,
#define PLCTypeAddr (MCUFLASHSIZE-MCUISPFLASHSIZE-512) // 即IAP倒数第一页.
unsigned char IAPFlashReadMode(unsigned int codeaddr) // 读模式
读取地址为PLCTypeAddr的内容,这个内容就是PLC版本号。
因为写PLC初始化内容时,第一个字符是0,如果不是,表示没有初始化,
if(PLCType!=0) // 第一次使用. IAP -- "FX1N PLC\r\n".
{
写入PLC初始化代码(第一个字符为0);
UartSendString(ArrFirstused);
}
else UartSendString(ArrPass);
上面发送的字符串笔记简单,
unsigned char code ArrFirstused[]="First used!\0";
unsigned char code ArrPass[]="PASS!\0";
初始化内容有,
ErasurePLC(ErasureALL); // 全部擦除 初始PLC程序为空
IAPFlashProgremMode(PLCTypeAddr,0); // 更新硬件版本号标识符标志
for(i=0;i<PLCTypeArrayLen;i++) // 更新硬件版本号
{ IAPFlashProgremMode(PLCTypeAddr+1+i,PLCTypeArray);
}
for(i=0;i<46;i++) // 写入PLC初始化代码 查询地址0x8000. 0x802e.
{ IAPFlashProgremMode(0x8000+i,OrderSend3);
IAPFlashProgremMode(0x802e+i,OrderSend4);
}
1、全部擦除 初始PLC程序为空
2、更新硬件版本号标识符标志(即第一个字符为0)
3、更新硬件版本号
4、写入PLC初始化代码 查询地址0x8000. 0x802e.
上面的代码都是比较好理解的。
******************************************
然后,
for(i=0;i<PLCTypeArrayLen;i++)
{ PLCTypeArray=IAPFlashReadMode(PLCTypeAddr+1+i);
}
把版本信息再读出来
UartSendByte(PLCTypeArray,PLCTypeArrayLen);
UartSendString("\r\n");
再发送出去。
*******************************************
while (((P1 & 0x03) == 0) && (RUN == 0))
{ FX1NProcessing(); // PLC下载通信处理
if ((P1 & 0x03) != 0)
{ delay_ms(20);
if ((P1 & 0x03) != 0)
{ RUN = 1;
IFMT= 0; //
ISPCR = 0x20; // SWBS= 0; 选择AP空间, SWRST = 1; 软复位
while (1) { ; }
}
}
}
这就是通讯判断函数,是不是接收上位机指令。
(P1 & 0x03) == 0) && (RUN == 0)
P1的第三个端口为低电平,同时设置的位变量RUN也是0时,就开始进入通讯核心函数FX1NProcessing。
if ((P1 & 0x03) != 0)
{
延时消抖;
从AP空间复位;
}
P1的第三个端口应该是一个按键,可以用来复位。复位函数是参考该芯片手册上的,不同芯片有区别。
******************************************
然后,
CODE_scan();
这是一个比较复杂的函数,
// PLC 代码 初次扫描,求出标号 Pn 地址函数main_PLC(); //
//-------------------------------------------------------------------------------------//
void CODE_scan (void)
理解这个函数,需要复习笔记07里的内容。
CODE_p = (unsigned char code *)CODE_START;
其中,
#define CODE_START 0x8000 // PLC执行代码缓冲区首地址
然后,
orderL = *CODE_p;
CODE_p++;
orderH = *CODE_p;
CODE_p++;
ppp = order & 0xfff;
可以看出,求出了偏移地址ppp。
具体如图所示,
从中可看出,利用 order & 0xfff求出偏移地址,
再利用orderH>>=4求出映射表(散转函数表)对应的位置。
然后很多if else if语句,这些语句暂时忽略。
最后,
CSP_Pn = (unsigned char code *)CODE_START;
这个语句需要结合PLC_main()一起理解,暂时略过。
*********************************************
oldbeginner 发表于 2013-11-26 09:28
继续PLC版本检测,
PLCType=IAPFlashReadMode(PLCTypeAddr); // 上电,核实 PLC 硬件版本号
最后就是 ,省掉非核心代码和非相关变量。
while (1)
{
input_IO();
main_PLC();
mov_to_old();
}
这段代码就是PLC正常执行时的循环。
首先,
*******************************************
void input_IO(void) // X输入,Y输出刷新
这个功能是必要的
因为只考虑x和y,其余不考虑,包括外设
void input_IO(void) // X输入,Y输出刷新
{
unsigned char i;
i = P2;
i = ((i << 1) & 0xaa) | ((i >> 1) & 0x55);
i = ((i << 2) & 0xcc) | ((i >> 2) & 0x33);
rX.BYTE = ~((P1 & 0x0f) | (i & 0xf0));
rX.BYTE = ~(i & 0x0f) & 0x0f;
P0 = rY.BYTE;
}
这种碟形算法还不会,因为不理解对应关系,暂时略过,打算把该算法换成容易理解的语句。
********************************************************
然后进入main_PLC函数,
void main_PLC (void)
{
CODE_p = (unsigned char code *)CODE_START;
Pi = 0x01;
do{
orderL = *CODE_p;
CODE_p++;
orderH = *CODE_p;
CODE_p++;
ppp = order & 0xfff;
(*key_list)();
} while((CODE_p < CODE_END) && (CODE_p != CODE_START));
}
因为在笔记07中理解过,在功能上基本都能理解。
需要增加的一个知识点就是函数指针,
http://www.amobbs.com/forum.php? ... 2715&extra=page%3D1上说过函数指针重要性,但是作为入门材料并不好,我推荐的文章是
http://wenku.baidu.com/view/28d11678a26925c52cc5bf71.html
有图有例子,非常好懂。
程序中定义如下,
code (*key_list)();
按照我的理解,改成
code void (*key_list)(void); 更好些
main_PLC 把 CODE_START和 CODE_END 之间的代码执行了一遍。因为CODE_p++会等于CODE_END,不是死循环,只是执行一遍。
*******************************************
void mov_to_old(void)
{
unsigned char i;
for (i=0; i<_X_BYTE; i++)
{ rX1.BYTE = rX.BYTE; }
for (i=0; i<_Y_BYTE; i++)
{ rY1.BYTE = rY.BYTE; }
}
根据定义
volatile TYPE_BIT_BYTE datarX , rY; //位元件 X,Y 存储位
volatile TYPE_BIT_BYTE datarX1, rY1; //位元件 X,Y 存储位上一步备份
从代码和功能上可以理解,备份作用。
不过rX1和rX和容易混淆,把rX1改成 rXbk或许好些。
***************************************
因为只理解x y LD SET OUT RST等几个变量和命令,并且不考虑外设,所以代码量小了很多,这样就完成了整个程序的第一遍理解。从理论上说,已经具备把理解的程序移植到STC12C516系列芯片上,因为该芯片已经有相应的PLC程序,所以下一步进行移植,可以完成:
1、对开源PLC程序的第二遍理解;
2、更新通讯协议;
3、和已有STC12C516程序的对比(现有的STC12C516程序没有详细的注解,文件安排也比较乱);
4、利用三菱梯形图软件完成LD X02
OUT Y5
END
能够把命令传递给STC12C516上,并且该单片机可以正常工作。 oldbeginner 发表于 2013-11-26 09:28
继续PLC版本检测,
PLCType=IAPFlashReadMode(PLCTypeAddr); // 上电,核实 PLC 硬件版本号
因为之前只理解LD和OUT,所以省去了很多相关代码,现在需要把PLC指令逐步添加,就有必要重新理解Code_scan函数了。
先把散转列表复习一下,
code (*key_list)()={
CMDFNC , // 0 (FNC应用指令)
CMDP , // 1 (P 应用指令)
LD , // 2 (LD指令, 2000+ppp, 扩展 Mp除外)
LDI , // 3 (LDI指令, 3000+ppp, 扩展 Mp除外)
AND , // 4 (AND指令, 4000+ppp, 扩展 Mp除外)
ANI , // 5 (ANI指令, 5000+ppp, 扩展 Mp除外)
OR , // 6 (OR指令, 6000+ppp, 扩展 Mp除外)
ORI , // 7 (ORI指令, 7000+ppp, 扩展 Mp除外)
CMDER, // 8 (多字指令,第二字及以后有效)
CMDER, // 9
CMDER, // A (多字指令,第二字及以后有效, 仅对M1536-M3071有效,需加偏移量200)
CMDER, // B (Pn指令, 仅对CJ,CALL有效)
OUTYM, // C (OUT指令, 仅对Y,M有效)
SETYM, // D (SET指令, 仅对Y,M有效)
RSTYM, // E (RST指令, 仅对Y,M有效)
CMDCH }; // F (纯单字指令)
************************************************************
void CODE_scan (void)
{
unsigned char pa, pb;
CODE_p = (unsigned char code *)CODE_START;
do{
orderL = *CODE_p;
CODE_p++;
orderH = *CODE_p;
CODE_p++;
ppp = order & 0xfff;
分别取出偏移地址和命令码(散转表数组下标)
继续,
if ((orderH>>4) <= 0x1)
{
if (ppp == 0x00f) // (END指令, 000F )
{
END();
}
(orderH>>4) <= 0x1,即命令码是0或者1,CMDFNC或CMDP命令,或者是END命令。因为END指令 00 0f,这时再通过验证偏移地址来确认是否是END指令。
继续,
else if (ppp <= 0x00e)
{
pa = (unsigned char)ppp;
if ((pa <= 0x001) || (pa == 0x00e)) ;
else if ((pa == 0x00a) || (pa == 0x00d))
{
CODE_p += 4;
}
else
{
CODE_p++;
CODE_p++;
}
}
原因不知道(还需要确认一下),肯定没有相对应的指令,跳过4个字节或2个字节,应该类似空行。
else if ((ppp>= 0x1c0) && (ppp <= 0x1cf))
{
pa = (unsigned char)(ppp - 0x1c0);
if ((pa <= 0x001) || (pa == 0x008) || (pa == 0x009)) ;
else
{
CODE_p++;
CODE_p++;
}
}
类似上面,继续执行或跳过2个字节
else if ((ppp >= 0x600) && (ppp < 0x800)) // 三字指令
{
CODE_p += 4;
}
判断出是三字指令,则指针+4。为什么?
看来要打断一下,找到原因再继续这里的理解。
http://www.amobbs.com/thread-3303497-1-1.html oldbeginner 发表于 2013-12-13 16:41
因为之前只理解LD和OUT,所以省去了很多相关代码,现在需要把PLC指令逐步添加,就有必要重新理解Code_sca ...
借鉴数学常用的反证法,如果(orderH>>4) > 0x1,是哪些指令?
单字指令
LD 2000+ppp ;(扩展 Mp除外)
LDI 3000+ppp ;(扩展 Mp除外)
AND 4000+ppp ;(扩展 Mp除外)
ANI 5000+ppp ;(扩展 Mp除外)
OR 6000+ppp ;(扩展 Mp除外)
ORI 7000+ppp ;(扩展 Mp除外)
Pn B000+(N) ; N=0-127
OUT C000+ppp ;(仅对Y,M有效)
SET D000+ppp ;(仅对Y,M有效)
RST E000+ppp ;(仅对Y,M有效)
纯单字指令
ANB FFF8
ORB FFF9
MPS FFFA
MRD FFFB
MPP FFFC
INV FFFD
NOP FFFF
************************************************
其余的都是(orderH>>4) <= 0x1),有哪些?
纯单字指令
END 000F
单字指令
CMDFNC , // 0 (FNC应用指令)
CMDP , // 1 (P 应用指令)
基本应用指令=(FNC.No.n+8)*2
D 应用指令 =(FNC.No.n+8)*2 +1
P 应用指令 =(FNC.No.n+8)*2 +1000
双字指令
OUT 0002 8000+ppp ;(仅对M8xxx有效)
OUT 0002 A000+ppp ;(仅对Mp有效)
SET 0003 8000+ppp ;(仅对M8xxx有效)
SET 0003 A000+ppp ;(仅对Mp有效)
RST 0004 8000+ppp ;(仅对M8xxx有效)
RST 0004 A000+ppp ;(仅对Mp有效)
OUT 0005 8000+ppp ;(仅对S有效)
SET 0006 8000+ppp ;(仅对S有效)
RST 0007 8000+ppp ;(仅对S有效)
PLS 0008 8000+ppp ;(仅对Y,M有效)
PLF 0009 8000+ppp ;(仅对Y,M有效)
MC 000A 8000+(N) 8000+ppp ;(仅对Y,M有效)
MCR 000B 8000+(N) ; N=0-7
RST 000C 8000+ppp ;(仅对T,C,Cp有效)
RST 000D 8m00+xx8n00+yy;(仅对 D 有效,包含Z,V)
LD 01C2 A000+ppp ;(仅对Mp有效)
LDI 01C3 A000+ppp ;(仅对Mp有效)
AND 01C4 A000+ppp ;(仅对Mp有效)
ANI 01C5 A000+ppp ;(仅对Mp有效)
OR 01C6 A000+ppp ;(仅对Mp有效)
ORI 01C7 A000+ppp ;(仅对Mp有效)
LDP 01CA 8000+ppp ;(扩展 Mp除外)
LDP 01CA A000+ppp ;(仅对 Mp有效)
LDF 01CB 8000+ppp ;(扩展 Mp除外)
LDF 01CB A000+ppp ;(仅对 Mp有效)
ANDP 01CC 8000+ppp ;(扩展 Mp除外)
ANDP 01CC A000+ppp ;(仅对 Mp有效)
ANDF 01CD 8000+ppp ;(扩展 Mp除外)
ANDF 01CD A000+ppp ;(仅对 Mp有效)
ORP 01CE 8000+ppp ;(扩展 Mp除外)
ORP 01CE A000+ppp ;(仅对 Mp有效)
ORF 01CF 8000+ppp ;(扩展 Mp除外)
ORF 01CF A000+ppp ;(仅对 Mp有效)
三字及五字指令
OUTTK 0600+(T)8000+xx 8000+yy
OUTCK 0E00+(C)8000+xx 8000+yy
OUTCp K 0000+(Cp) 8000+xx 8000+yy 8000+zz 8000+ww//忽略
**************************************************************
所以现在可以理解为何当 if ((orderH>>4) <= 0x1) 时,会有很多if。。。else if。。。else if。。。语句,是因为不同类型指令字节长度不一样。
研究一个例子看看,
例如 OUT T1 K5
指令码应该是 0601 8000 8005
再回到Code_scan函数,
else if ((ppp >= 0x600) && (ppp < 0x800)) // 三字指令
{
CODE_p += 4;
}
因为ppp=0x601,所以执行这句;这样指针就跳过8000 和8005指令,指向下一条指令。
这样证明Code_scan只是检验命令格式是否正确。
用类似的方法可以把其它命令都确认一下。但是没有返回信息,感觉这样子确认有些多余。
如果指令格式不对(很简单只考虑指针是否超出,并不全面),会有
if (CODE_p >= CODE_END)
{
CODE_ERROR =1;
}
这样在主函数中,
if (CODE_ERROR == 0)
{
main_PLC();
}
Code_scan作用有限。
但是从上面OUT T1 K5例子中引出了一个类似的但更复杂函数,
code (*key_list)()={
CMDFNC , // 0 (FNC应用指令)
。。。。
}
void CMDFNC(void) // 0 (应用指令)
{
很多代码
。。。
}
oldbeginner 发表于 2013-12-14 15:45
借鉴数学常用的反证法,如果(orderH>>4) > 0x1,是哪些指令?
单字指令
/********************CMDFNC指令散转*******************************************/
void CMDFNC(void) // 0 (应用指令)
{
END指令();
(ppp <= 0x00e)();
((ppp>= 0x1c0) && (ppp <= 0x1cf))();
三字指令T();
三字指令C();
基本应用指令();
D应用指令();
}
还是按照例子OUT T1 K5来理解,
这里会选择
三字指令T()
else if ((ppp >= 0x600) && (ppp < 0x800)) // 三字指令
{
WR_TK(ppp - 0x600); // OUTT,K 0000+(T)8000+xx 8000+yy
}
因为ppp=0x601,则调用 WR_TK(1);
其中
void WR_TK(unsigned int a) // ,(K值写入T)
{
代码();
}
至此,OUT T1 K5才得到执行。
********************************************
如果是基本应用指令,则还要复杂一些,
在CMDFNC中,还要散转一下,调用
(*key_list_3)(); //应用指令散转
code (*key_list_3)() ={
FNC_CJ , // 0
FNC_CALL, // 1
FNC_SRET, // 2
FNC_IRET, // 3
。。。。
}
根据下标,再调用相应函数。
还有其它情况的散转函数,这里暂时不展开,目的只是先把大脉络理解,然后把PLC指令逐步加入到笔记19中,笔记19将是51下的PROTEUS仿真,并且能接收三菱梯形图指令。 mark,楼主很牛哦! mark,楼主很牛!顶一个.好资料
页:
[1]