我的第二个单片机开源项目——全比例模型遥控器摇杆位置采样与PPM编码信号输出程序
本帖最后由 joyrus 于 2012-6-26 11:45 编辑开源项目名称:全比例模型遥控器摇杆位置采样程序、PPM编码信号输出程序
设计目的:用于实现对遥控器摇杆电位器的电压采样,转换为数字信号进行PPM编码为以后的PCM编码做准备。其实这里实现PPM编码的目的只是为了兼容PPM接收机而已,PCM编码为纯数字编码,与PPM是没有联系的,不必通过PPM在转PCM。所以实际上PPM和PCM是完全不相关的两套系统。
正在做准备工作,先学习有关ADC的相关知识和PPM编码特点。
这是本人使用的平台在淘宝有售,本人也是在淘宝购买的,有兴趣的朋友可以去了解一下:
1. 国产PICKit3仿真调制器,价格非常实惠!这是我在淘宝上找到的最便宜的价格了!我现在的就是国产的,买的时候比这个价格还贵一点,功能和原厂的完全一样!
2. Microchip原装PICKit3仿真调制器,也是比较便宜的价格!有原厂情节的可以买原厂的。
3. 这个PIC16F877A开发板比我的要好很多,有配箱子和编程器,价格才200出头有需要的朋友可以考虑!
4. 这家的PIC16F877A的开发实验板也不错,价格更低,配置也很多的!
大家注意开发板要和仿真调试器一起配合使用才能真正的在线调试程序。
本人对设计前所做的一些设计要点归纳
本帖最后由 joyrus 于 2012-6-21 19:19 编辑要点:
1. 遥控器摇杆并没有占用电位器的所有行程,所以其采样电压并非0-5V,这样的话就需要使用单片提供的Vref+/-两个参考电压引脚来框定摇杆满行程的电压。另外还要考虑微调叠加的总电压。当然如果是独立微调电位器暂时就不用考虑。
2. 如果设计为4通道全比例那么需要4个ADC通道,如果微调独立则需要另外4个ADC通道,微调的另一种方式为机械按钮,这样就需要8个普通IO口,节省下的4个ADC通道可用于遥控器通道扩展。
3. PPM编码的特点:在以20ms为周期(每一帧)的信号中包含了所有通道的信息,每个通道占用最多2.5ms的脉冲宽度,这样一帧信号中最多只能纳入8个通道的信息。这就是为什么Futaba的遥控器在PPM模式下仅支持8通道的原因。
4. 单片机需要在20ms内完成所有通道的ADC转换并将数据处理后输出标准的PPM编码。如果以最简单的不带任何存储与调整功能的遥控器为例,其对单片机的处理能力主要由遥控器通道的数量决定的,当然同时还有能够使用的晶振频率,概念就是电脑CPU的频率一样。
5. 如果使用单片机内部的ADC,那么在获得结果后可以直接用于转换输出PPM编码,但这样也会占用单片机(MCU)的处理时间,因为ADC采样需要时间,同时采样后因对数据进行一些基本的校验,这都需要时间,即使MCU有8通道的ADC也是一个个轮流采样,而不是同时进行的。如果用独立的ADC采样IC,那么采样的过程就可以和MCU并行工作,但问题又来了。ADC采样后的数据需要传输到MCU来处理,这需要程序对数据进行缓存。本人认为在对采样位数要求不高(一般8位-10位),遥控器通道数比较少的情况下,应该使用MCU内建的ADC。而在通道数比较多,采样位数12位甚至更高时要考虑使用独立的ADC部件。最佳的方式是用一颗MCU独立采样并处理采样数据,有专门的共享存储器来存储数据供其他MCU使用并输出需要的更高级的数据格式。
6. ADC采样位数问题,PIC16F877A有10位ADC,而8位MCU需要处理超过8位的数据需要分两步来做,所以是选择8位还是10位ADC模数在实际编程时将做以比较。如果MCU(单片机)的处理能力有足够的冗余,内存空间足够,程序代码空间足够(如果使用查表确定转换关系的话,表长度将增加到原来的4倍),考虑使用10位ADC模数转换。 本帖最后由 joyrus 于 2012-6-23 15:07 编辑
根据预先的系统分析(非专业),生成如下设计思路:
1. 遥控通道按顺序ADC模数采样,每个通道连续采样2次,总共用时约50us。保存两次采样结果,对数值进行校验。校验方法设计为,预先假定两次采样的最大误差值(以超人类的方式高速移动摇杆计算得到),如果两次采样误差小于最大误差值,说明采样数据合格,取第二次采样的值。如不合格则取两次采样值的平均值。最后查表得到相应的脉宽计数值(也可以通过计算,视单片机的速度而定),其实这个脉宽计数值需要转换为TMR1的预设值,下面将讲到。每个通道的TMR1的预设值都预存在内存中。TMR1是16位定时器,每个预设值需要2个字节的空间,所以表长至少512字节(256x2),如果用10位ADC得到1024个值,表长就要到达1024×2=2048字节,也就是2KB。有些单片机总共就只有2KB的程序空间,这就不能用10位ADC,或者不能用查表的方法来提高程序的效率了。也可以通过两次查表来减少表的长度,因为在高8位只有8个不同的值(8x256)。这样即使10位ADC,表长也就只要8+256个字节。
(本人在对实际PPM信号进行测量后确定其输出的每个通道脉冲宽度变化在1ms左右也就是1000us,对于8位采样后得到的256个结果1000/256是除不尽的,为了方便计算取1024us作为输出的最大值,这样就可以被256除尽,并且不需要查表,可以直接通过计算得到结果。因为对应的关系是固定的×4关系,只要对ADC的结果连续左移两次,并把最高的两位依次移入高8位最末两位即可,不仅节省了程序空间,而且使用的指令数量也大大的减少了。)
2. 设置16位TMR1定时器/计数器溢出中断响应,在中断中调用第一步预存的TMR1预设值来输出PPM脉冲。原理如下,使用4MHz晶振,指令周期1us。20ms的PPM周期已经确定,遥控器通道数也已经确定假设4通道,而每个通道脉冲信号占据的宽度可以设定为3ms,这样就可以得到固定的每个通道的脉冲起始位置所需要的TMR1预设计数值,然后每个通道的脉冲宽度计数值也已经在第一步中被事先保存好了,只要在中断中调用即可。由于使用了中断可以相对精确的通过计算中断中的指令周期而修正计数误差。16位TMR1定时器/计数器的使用有些特点,看一下PIC16F877A的手册或者相关的书籍即可了解。
3. 由于TMR1的预设值是独立保存在指定的内存区域,所以即使在采样的过程中如果出现中断响应也不会影响最终的PPM输出。
4. 在实现了PPM信号输出后,再进行一些辅助功能的设计,比如最基本的遥控器通道的正反向。
以上为预先设想设计思路,可能存在错误和修正。程序将一步一步的来写。 今天已经完成了8位ADC采样的程序,以及两次采样的数据校验程序如下:
;ADC转换子程序:
;**********************************************************************
ADC_doing
bsf ADCON0,GO ;开始ch0的ADC
btfscADCON0,GO ;ADC是否结束
goto$-1 ;等待ADC结束
;保存ADC结果
movlw ad_data_addr ;取ADC数据缓冲区首地址
movwf FSR ;FSR相对寻址
movf ad_data_point,w ;取相对位移指针
addwf FSR,f ;相对地址+位移指针
movf ADRESH,w ;取ADC转换结果
movwf INDF ;放入ADC转换结果
;incf ad_data_point ;指针位移+1指向下一个缓冲单元,这一步在每个通道的主程序中完成
return
;***********************************************************************
通道采样主程序如下:
clrfad_data_point ;初始化ADC数据缓冲区指针
ADC_ch0
;选择通道0
movlwb'00000001'
movwfADCON0
;采样并保存
callADC_doing ;第一次ADC转换
incfad_data_point ;指针位移+1指向下一个缓冲单元
callADC_doing ;第二次ADC转换
incfad_data_point ;指针位移+1指向下一个缓冲单元
;数据校验
movf ad_data_addr,w ;第一次数据->w
subwfad_data_addr+1,w ;第二次数据-第一次数据->w
addlwad_data_check_number ;最大误差值+w,若C=1进位,数据不合格需要处理
bnc ADC_ch1 ;C=0则表明数据正确,下一通道采样
;处理数据不合格
movf ad_data_addr,w ;第一次数据->w
addwfad_data_addr+1,f ;第二次数据+第一次数据->第二次数据位
rrf ad_data_addr+1,f ;两次数据和/2求平均,放入第二次数据位
ADC_ch1
.......
.......
....... 以下是实测的遥控器PPM信号(PPM编码格式),在示波器中可以看到完整的一帧PPM脉冲信号。中间低电平是一帧信号的剩余空间,直到下一帧信号的第一个上升沿结束。虽然感觉低电平有很长的空间可以在放入过多的通道信号,实际上看到的这些脉冲信号都没有在最大的脉冲宽度,如果都在最大越2.5ms的宽度时就看不到这样大的空白区域了。
把信号展开:
可以看到最前面的4个脉冲为实际使用的通道,调整的是低电平的宽度,后面3个脉冲是固定在最小脉冲宽度,实际上就是说这3个通道是这台4通道遥控器没有使用到的。这些数据是非常有用的,为下面的程序中的参数提供了实际指导。 根据设计思路对本次设计至关重要的TMR1定时器进行了实验性的编程,配合TMR1定时器溢出中断,对TMR1L和TMR1H的赋值来实现对16位TMR1定时器赋值,实现了周期可控的方波输出。程序如下:
为了方便使用了PIC提供MPLAB IDE提供的程序模板,可以不用自己写中断现场保护代码。
;***** VARIABLE DEFINITIONS
w_tempEQU 0x7D; variable used for context saving
status_temp EQU 0x7E; variable used for context saving
pclath_temp EQU 0x7F; variable used for context saving
;**********************************************************************
ORG 0x000 ; processor reset vector
nop ; nop required for icd
goto main ; go to beginning of program
ORG 0x004 ; interrupt vector location
movwf w_temp ; save off current W register contents
movf STATUS,w ; move status register into W register
movwf status_temp ; save off contents of STATUS register
movf PCLATH,w ; move pclath register into w register
movwf pclath_temp ; save off contents of PCLATH register
btfss PIR1,TMR1IF ;是否TMR1溢出中断,TMR1IF=1则处理中断
goto Go_main ;不是TMR1溢出中断,退出中断
bcfPIR1,TMR1IF ;清除TMR1溢出中断标志
bcfT1CON,TMR1ON ;暂停TMR1
movlw 0xEC ;TMR1赋初值
movwf TMR1H
movlw 0x78
movwf TMR1L
bsfT1CON,TMR1ON ;启动TMR1
btfsc PORTD,1 ;输出是低电平?Y则输出高电平
goto Get_low ;是高电平,则输出低电平
bsfPORTD,1 ;输出高电平
goto Go_main
Get_low
bcfPORTD,1 ;输出低电平
Go_main
movf pclath_temp,w ; retrieve copy of PCLATH register
movwf PCLATH ; restore pre-isr PCLATH register contents
movf status_temp,w ; retrieve copy of STATUS register
movwf STATUS ; restore pre-isr STATUS register contents
swapf w_temp,f
swapf w_temp,w ; restore pre-isr W register contents
retfie ; return from interrupt
main
;Bank1 初始化TMR1中断
;****************
bankselPIE1
movlwb'00000001'
movwfPIE1
bcf TRISD,1 ;定义PORTD1为输出
;Bank0 初始化TMR1
;***************
bankselT1CON
movlwb'00000001'
movwfT1CON
movlwb'11000000'
movwfINTCON
bcf PORTD,1
goto$;循环输出固定周期的方波,周期由TMR1的初值决定
END ; directive 'end of program' 本帖最后由 joyrus 于 2012-6-25 14:26 编辑
程序已经完成了。
实现了4通道遥控器的最基本功能,输出4通道PPM信号。但没有对信号宽度进行严密的延时补偿,也就是说,在进入中断关闭和再次打开TMR1之间不同的通道数据处理所花费的指令周期,并没有在计算中给予负补偿。根据简单的计算,最小补偿值为约16us,最大的补偿值约为26us,可以将经过分支判断后将每条分支所花费的指令周期统一到26us(插入nop指令),之后统一在中断外事先负补偿26us即可。如果不补偿则误差最大为1024/26×100%约为2.539%。
在这个项目中总结了一些经验教训主要是有关于PIC单片机中断处理的经历,以及个别指令对STATUS状态寄存器标志位的影响。我将总结内容另开一篇集中收集我在日后的开发项目中遇到的各种经验教训。
我在PIC单片机开发中总结的经验教训-有关中断、指令、处理16位数据的使用注意事项等
在整个过程中我发现在PIC单片机中对16位数据的处理比较麻烦,往往需要比8位数据多2倍,甚至某些运行还要花费更多的指令周期,而且在程序的编写和整理的过程中也比较容易出错!
稍后我将程序做一些整理就发上来。 本帖最后由 joyrus 于 2012-6-25 15:01 编辑
先上几张输出的PPM波形图,和视频:
完整的一帧4通道20ms周期波形图:
三通道ADC采样接5V满输出,其他通道接地0输出。
展开后的波形
三和四通道ADC采样接5V满输出,其他通道接地0输出。
信号流:
http://player.youku.com/player.php/sid/XNDE5NDkxNTky/v.swf 本帖最后由 joyrus 于 2012-6-26 11:39 编辑
详细程序如下:
第一部分,配置单片机,定义变量和常量
所有程序内容:
;**************************************************
;程序功能说明
;本程序通过对4ADC引脚的输入电压采样保存
;进行转换为全比例遥控模型遥控器的PPM信号
;由PORTD1输出,可送至射频电路调制发射,也可以作为电脑模拟器的输入信号。
;本程序有很大的优化空间和可能存在安全漏洞,这个程序只是本人的学习过程,和大家共同探讨的载体
;本人联系方法:沈毅;joyrus@163.net,www.joyrus.com,QQ:15953321
;**************************************************
#include <p16f877A.inc>;使用PIC16F877A单片机
;根据需要在这里配置单片机,也可以在MPLAB IDE中配置
;__CONFIG
;*******************定义变量
w_tempEQU 0x7D ; variable used for context saving
status_temp EQU 0x7E; variable used for context saving
pclath_temp EQU 0x7F ; variable used for context saving
cblock 0x20
ad_data_buff: 8 ;采样数据缓冲区每通道2个数据
;ch0-ch4顺序放置
ad_data_point ;采样数据缓冲区偏移指针
ch_ppm_buff: 8 ;每个通道PPM计数初值数据缓冲区每通道16位数据
;ch0-ch4顺序放置(每个通道PPM脉冲的低电平)
ch_ppm_sel ;输出PPM通道选择
ch_ppm:2 ;所有通道PPM计数计算时的临时存储区
ppm_head_flag ; PPM头脉冲标志位(每个通道PPM脉冲的高电平)
ppm_tail_L ; PPM尾脉冲宽度所有通道(实时计数需要计算)
ppm_tail_H ; PPM尾脉冲宽度所有通道(实时计数需要计算)
ppm_interval_origin:2 ; PPM每个通道的固定间隔宽度488us
;TMR1计数到0x0000时溢出中断故赋初值为
;65536-488us(0xFE18)在程序中初始化
endc
;******************定义常量
ad_check_no EQU 0x0A ;两次采样数据最大允许误差值(可根据实际情况更改)
ch0 EQU 0x00;ch0数据定位偏移指针(代替16位数据的间接寻址)
ch1 EQU 0x02;ch1数据定位偏移指针
ch2 EQU 0x04;ch2数据定位偏移指针
ch3 EQU 0x06;ch3数据定位偏移指针 第二部分,TMR1中断处理程序,输出所有通道的PPM信号,只有通道尾脉冲的计数值是在中断中实时计算的。
;**********************************************************************
ORG 0x000 ; processor reset vector
nop ; nop required for icd
goto main ; go to beginning of program
ORG 0x004 ; interrupt vector location
movwf w_temp ; save off current W register contents
movf STATUS,w ; move status register into W register
movwf status_temp ; save off contents of STATUS register
movf PCLATH,w ; move pclath register into w register
movwf pclath_temp ; save off contents of PCLATH register
;开始处理TMR1溢出中断
bankselSTATUS
btfssPIR1,TMR1IF ;是否TMR1溢出中断,TMR1IF=1则处理中断
gotoGo_main ;不是TMR1溢出中断,退出中断
bcf PIR1,TMR1IF ;清除TMR1溢出中断标志
bcf T1CON,TMR1ON ;暂停TMR1
;在进入中断和暂停TMR1前有约14us的延迟
;这个时间应该在最终完成所有程序后得到负补偿
;无论是在头脉冲,尾脉冲,还是固定间隔宽度
;判断是否脉冲头
;***********************
btfscppm_head_flag,0; PPM通道脉冲头标志为0则输出PPM通道脉宽/2us
gotoGet_ppm_head ;取PPM通道脉冲头/2us
Get_ppm_ch ;输出PPM通道脉宽
Tail_ppm ;/26us
;通道尾脉冲也视作最后一个正常通道来处理,同时完成标志/计数的复位
;***********************
btfssch_ppm_sel,4 ;判断是否通道尾输出/2us
gotoch3_ppm ;转去通道3/2us
clrfch_ppm_sel ;重置通道选择
bsf ch_ppm_sel,0
;********************************计算尾脉冲
;取尾计数值的补数/把下面的减法变加法(方便统一理解程序)
comfppm_tail_L,f
comfppm_tail_H,f
movlw0x01
addwfppm_tail_L,f
skpnc ;2us
incfppm_tail_H,f
;用常数减计数值求TMR1计数初值(用补数变加法)
movlw0xE0 ; 0xE0(65536-20000)低8位
addwfppm_tail_L,f ;固定的20ms间隔低8位
skpnc ;判断进位/2us
incfppm_tail_H,f ;高8位加1
;计算高8位
movlw0xB1 ; 0xB1(65536-20000)高8位
addwfppm_tail_H,f ;固定的20ms间隔低高8位
;************************
;取通道尾的PPM计数初值低8位
;************************
movfppm_tail_L,w ;取PPM计数初值数据
movwfTMR1L ;送TMR1L
;取通道尾的PPM计数初值高8位
;************************
movfppm_tail_H,w ;取PPM计数初值数据
movwfTMR1H ;送TMR1H
;清尾数
clrfppm_tail_L
clrfppm_tail_H
;
gotoStart_tmr1 ;2us
ch3_ppm;/14us
;通道3
;************************
btfssch_ppm_sel,3;判断是否通道3输出/2us
gotoch2_ppm ;转去通道2/2us
rlf ch_ppm_sel,f ;通道选择,准备下一通道
;取通道3的PPM计数初值低8位
;************************
movfch_ppm_buff+ch3,w ;取PPM计数初值数据
movwfTMR1L ;送TMR1L
;累计PPM尾
addwfppm_tail_L,f ;累计PPM脉冲尾低8位
skpnc ;判断进位/2us
incfppm_tail_H,f ;高8位加1
;
;取通道3的PPM计数初值高8位
;************************
movfch_ppm_buff+ch3+1,w ;取PPM计数初值数据
movwfTMR1H ;送TMR1H
;********************************累计PPM尾
addwfppm_tail_H,f ;累计PPM脉冲尾高8位
;************************
gotoStart_tmr1 ;2us
ch2_ppm;/14us
;通道2
;************************
btfssch_ppm_sel,2;判断是否通道2输出
gotoch1_ppm ;转去通道1
rlf ch_ppm_sel,f ;通道选择,准备下一通道
;取通道2的PPM计数初值低8位
;************************
movfch_ppm_buff+ch2,w ;取PPM计数初值数据
movwfTMR1L ;送TMR1L
;累计PPM尾
addwfppm_tail_L,f ;累计PPM脉冲尾低8位
skpnc ;判断进位
incfppm_tail_H,f ;高8位加1
;
;取通道2的PPM计数初值高8位
;************************
movfch_ppm_buff+ch2+1,w ;取PPM计数初值数据
movwfTMR1H ;送TMR1H
;********************************累计PPM尾
addwfppm_tail_H,f ;累计PPM脉冲尾高8位
;************************
gotoStart_tmr1
ch1_ppm;/14us
;通道1
;************************
btfssch_ppm_sel,1;判断是否通道1输出
gotoch0_ppm ;转去通道1
rlf ch_ppm_sel,f ;通道选择,准备下一通道
;取通道1的PPM计数初值低8位
;************************
movfch_ppm_buff+ch1,w ;取PPM计数初值数据
movwfTMR1L ;送TMR1L
;累计PPM尾
addwfppm_tail_L,f ;累计PPM脉冲尾低8位
skpnc ;判断进位
incfppm_tail_H,f ;高8位加1
;
;取通道1的PPM计数初值高8位
;************************
movfch_ppm_buff+ch1+1,w ;取PPM计数初值数据
movwfTMR1H ;送TMR1H
;********************************累计PPM尾
addwfppm_tail_H,f ;累计PPM脉冲尾高8位
;************************
gotoStart_tmr1
ch0_ppm;/14us
;通道0
;************************
btfssch_ppm_sel,0;判断是否通道0输出
goto$+1 ;匹配指令周期数
rlf ch_ppm_sel,f ;通道选择,准备下一通道
;取通道0的PPM计数初值低8位
;************************
movfch_ppm_buff+ch0,w ;取PPM计数初值数据
movwfTMR1L ;送TMR1L
;累计PPM尾
addwfppm_tail_L,f ;累计PPM脉冲尾低8位
skpnc ;判断进位
incfppm_tail_H,f ;高8位加1
;
;取通道0的PPM计数初值高8位
;************************
movfch_ppm_buff+ch0+1,w ;取PPM计数初值数据
movwfTMR1H ;送TMR1H
;********************************累计PPM尾
addwfppm_tail_H,f ;累计PPM脉冲尾高8位
;************************
gotoStart_tmr1
Get_ppm_head;/9us
;输出PPM通道脉冲头
;取PPM脉冲头宽度低8位
;************************
movlw0x0C ; PPM头脉冲宽度65536-500us=(0xFE0C)
movwfTMR1L ;送TMR1L
;累计PPM尾
addwfppm_tail_L,f ;累计PPM脉冲尾低8位
skpnc ;判断进位/2us
incfppm_tail_H,f ;高8位加1
;
;取PPM脉冲头宽度高8位
;************************
movlw0xFE ; PPM头脉冲宽度65536-500us=(0xFE0C)
movwfTMR1H ;送TMR1H
;********************************累计PPM尾
addwfppm_tail_H,f ;累计PPM脉冲尾高8位
;*************************
Start_tmr1
bsf T1CON,TMR1ON ;启动TMR1
;输出PPM信号
;**************************
btfscppm_head_flag,0
gotoOutput_head ;输出高电平PPM通道脉冲头
Output_ch_ppm
bcf PORTD,1 ;输出低电平PPM通道脉宽,包括尾脉冲
gotoGo_clear
Output_head
bsf PORTD,1 ;输出高电平通道脉冲头
;***************************
Go_clear
;输出PPM脉冲头标志位反转,输出一次脉冲头,再输出一次PPM脉冲宽度
comfppm_head_flag,f
Go_main
movf pclath_temp,w ; retrieve copy of PCLATH register
movwf PCLATH ; restore pre-isr PCLATH register contents
movf status_temp,w ; retrieve copy of STATUS register
movwf STATUS ; restore pre-isr STATUS register contents
swapf w_temp,f
swapf w_temp,w ; restore pre-isr W register contents
retfie ; return from interrupt
页:
[1]
2