温馨提示:请勿直接抄袭代码,代码仅供学习交流,核心功能语句不公开,且进行脱敏和插入错误语句,防止抄袭,如果你是一名本科生,需要完成对应课程设计,这里建议你好好学习C语言编写逻辑,否则文字内容和结构将晦涩难懂,想直接抄袭的话,是不可能的!!!
别抄袭!别抄袭!别抄袭!
项目介绍
要求:
- D/A转换固定频率波形输出:按键1按下输出40Hz三角波(或锯齿波、梯形波);按键2按下输出60Hz正弦波;
- 在LCD上实时显示当前波形的名称、频率及电压幅值;
- 在仿真示波器及实际示波器中调试出正确的波形;
- 可调频率:读取可调电位器的电压值,经A/D转换后可用于调节波形频率。频率范围10-100Hz;
- 上位机控制:可以通过串口调试助手进行参数设置和显示;
项目环境介绍
单片机类型: PIC16F887
开发环境: MPLAB X IDE v5.00 + Proteus 8 Professional
开发语言及编译器: C语言 +xc8 v2.4
硬件条件: *PCF8591、按键、LCD
项目结构介绍
项目中,主要文件包为software和hardware,其中:
- software
- wave.h
- hardware
- adc.c
- adc.h
- iic.c
- iic.h
- lcd.c
- lcd.h
- usart.c
- usart.h
项目结构图
项目内容
IIC技术
IIC是一种通讯协议,其本质属于串行通讯。其利用SCL,SDL,GND三根线实现设备之间的通讯,在STM32中可以通过标准库函数很轻易调用IIC通讯;通常来说,单片机中的IIC和SPI协议支持的子模块都是同一个硬件模块。在PIC单片机中,其IIC通讯需要从底层的寄存器出发设置参数,书写更为不便,因此,基于其特性,可以利用C语言将其寄存器调用封装为标准库函数,便于后续调用,后续的类似功能例如ADC也采用这类的方法书写。参照STM32中的书写,C语言需要构建.c和.h文件,且需要通过include进行关联。
IIC代码如下
下面是.c文件
#include "iic.h"
void IIC_Initial(void){
TRISC3=1;TRISC4=1; //RC3 RC4设置为输入
SSPEN=1;
SSPCON=0B00111000;
SMP=0;
SSPADD=1;
}
//下面IIC通讯中很容易进入死循环
void IIC_Send_Data(unsigned int data){
SEN=1; //产生起始位
while(SEN==1); //检测起始位完成
SSPBUF=0B10010000; //发送有效地址激活器件,PCF8591激活字节
while(R_nW==1); //检测数据是否发送完毕
while(SSPIF==0){
wait++;
if(wait>250){ wait=0;break;}
};
wait=0;
while(ACKSTAT==1){
wait++;
if(wait>250){ wait=0;break;}
}; //检测是否应答
SSPBUF=0x40; //发送0b01000000,模拟量输出使能
while(R_nW==1);
while(SSPIF==0){
wait++;
if(wait>250){ wait=0;break;}
};
wait=0;
while(ACKSTAT==1){
wait++;
if(wait>250){ wait=0;break;}
}; //检测是否应答
wait=0;
SSPBUF=data; //发送需要发送的数据
while(R_nW==1);
while(SSPIF==0){
wait++;
if(wait>250){ wait=0;break;}
};
wait=0;
while(ACKSTAT==1){
wait++;
if(wait>250){ wait=0;break;}
}; //检测是否应答
PEN=1; //停止位
}
下面是.h文件
#ifndef __IIC_H
#define __IIC_H
#define SCL RC3
#define SDA RC4
#include <xc.h>
//设备的地址定义如下,可以根据设备侧的实际地址设置而改变
#define PCF_ADDR 0X90
char wait=0;
void IIC_Initial(void);
void IIC_Send_Data(unsigned int data);
#endif
ADC技术
ADC技术很简单,就是A->D的一种技术,PIC16F887中的ADC属于逐次逼近型ADC,且位数为10位。对于一般性应用来说,够用了。逐次逼近型ADC本质就是一堆数字逻辑器件加上DAC模块和寄存器,在单片机中,其是作为单片机的一个硬件模块。通过配置寄存器可以实现ADC功能。
#include "adc.h"
void AD_Initial(void)
{
ANSELH=0;//模拟选择
OPTION_REG=0B10000000;//禁止弱上拉,下降沿触发中断
TRISE0=1;//模拟口输入,用于ADC采样
TRISB=0B11111100;//将检测外部电平变化全部初始化,并且
IOCB=0B11111100;//电平中断变化使能
TRISD=0B00000000;//LCD部分的所有端口都配置为输出,控制LCD
PR2=48;//初始波形周期设置
RBIE=1;
PEIE=0;
TMR2IE=1;
}
unsigned int AD_SUB(unsigned char k)
{
unsigned char i;
ADCON1=0B10000000;
ADCON0=0B01000001;
ADCON0=ADCON0|(k<<2);
for(i=0;i<3;i++) NOP();
GO=1;
while(GO==1);
return((ADRESH<<8)+ADRESL);
}
//BCD转换函数
void BCD(unsigned int R1)
{
QW=0;BW=0;SW=0;GW=0;
while(R1>=1000)
{
R1-=1000;QW++;
}
while(R1>=100)
{
R1-=100;BW++;
}
while(R1>=10)
{
R1-=10;SW++;
}
GW=R1;
}
void __FUZHI(void)
{
if(F==0)
{
GG[2]='5';GG[4]='0';GG[5]='0';
}
if(F==1)
{
GG[2]='2';GG[4]='5';GG[5]='0';
}
if(F==2)
{
GG[2]='1';GG[4]='2';GG[5]='5';
}
if(F==3)
{
GG[2]='0';GG[4]='6';GG[5]='3';
}
}
void __ZHOUQI(void)
{
PR2=(AD_SUB(5)>>2);
if(PR2>240) PR2=240;
if(PR2<4) PR2=4;
T=(PR2+1)*(-0.58);
T=(T+54)*10;
// f=(1.0/T);
if(T<=100){
GG[11]='0';GG[12]='1';GG[13]='0';
}
if(T>=500){
GG[11]='0';GG[12]='7';GG[13]='4';
}
else{
BCD(T);
GG[11]=QW+'0';
GG[12]=BW+'0';
GG[13]=SW+'0';
}
}
下面是.h文件,上面是.c文件
#ifndef __ADC_H
#define __ADC_H
#define _XTAL_FREQ 4000000
#include <xc.h>
#include "lcd.h"
extern unsigned int QW,BW,GW,SW;
unsigned int QW,BW,GW,SW;
unsigned int A=0,M=0;
unsigned int F=0;
unsigned int T;
double f;
void AD_Initial(void);
unsigned int AD_SUB(unsigned char k);
void BCD(unsigned int R1);
void __FUZHI(void);
void __ZHOUQI(void);
#endif
LCD显示技术
LCD主要是通过位操作,其通过配置单片机I/O口和控制高低电平来实现与LCD通讯,从而实现对LCD的控制。
#include "lcd.h"
#define _XTAL_FREQ 4000000
// 说明时钟频率 在下面调用 __delay_us(char a);__delay_ms(char a); (注意这里delay前面是两条下划线)
//初始化语句
void LCD_Init(void){
// INTCON=0;//先关闭中断使能
TRISD=0X00;//设置所有PORT所有引脚为输出
DELAY_MS(20);
LCD_WRITE_4(0b0011,COM);//发送三次控制序列
DELAY_MS(1);
LCD_WRITE_4(0b0011,COM);
DELAY_US(10);
LCD_WRITE_4(0b0011,COM);
DELAY_US(10);
LCD_WRITE_4(0b0010,COM);//命令6,4位数据格式
LCD_BUSY();
LCD_WRITE(0b00101000,COM);//命令6,功能设置,设置行数N,点阵F
DELAY_US(10);
LCD_WRITE(0b00001100,COM);//命令4,显示控制,打开显示,不显示光标,关闭闪烁
DELAY_US(10);
LCD_WRITE(0b00000001,COM);//命令1,清屏
DELAY_MS(2);
LCD_WRITE(0b00000110,COM);//命令3,输入模式设置,光标右移,禁止整体移动
DELAY_MS(2);
}
//写入半个字节
void LCD_WRITE_4(char R1,char FLAG){
//写入四位字节
LCD_RW=0;//写模式
DELAY_US(2);
LCD_RS=FLAG;//选择输入命令还是数据
DELAY_US(2);
LCD_E=1;//抬高使能引脚
DELAY_US(2);
PORTD|=R1;
DELAY_US(2);
LCD_E=0;//下降沿写入数据
DELAY_US(2);
LCD_RS=0;//调整为默认状态命令
DELAY_US(2);
PORTD&=0XF0;//复位
}
//===============================================
//读取数据
char LCD_READ(void)
{ unsigned char R1;
LCD_RS=0; //寄存器选择,0为命令
DELAY_US(2);
LCD_RW=1; //读为1
DELAY_US(2);
LCD_E=1; //使能,下降沿有效
DELAY_US(2);
R1=0; //短延时
R1=(PORTD<<4)&0xF0; //读PORTD的高4位给R1
LCD_E=0;
DELAY_US(2);
LCD_E=1; //使能,下降沿有效
DELAY_US(2);
R1|=(PORTD&0x0F); //读PORTD的低4位,R1的高4位不变
LCD_E=0;
DELAY_US(2);
LCD_RW=0;
DELAY_US(2);
return(R1);
}
//===============================================
//忙检测
void LCD_BUSY(void)
{ unsigned char R1;
while(1)
{ R1=LCD_READ(); //读寄存器
DELAY_US(5);
if((R1&0x80)==0x00) //最高位为忙标志
break;
};
}
//===============================================
//发送一整个字节,带忙检测
void LCD_WRITE(char R1,char FLAG)
{ char R2;
LCD_BUSY();//忙检测,LCD反应速度没有单片机那么快,忙检测保证LCD可以正常接收数据;
R2=R1&0xF0;//先赋值高八位
R2=R2>>4;//把高八位右移后作为四个bit组成半个字节发送出去
LCD_WRITE_4(R2,FLAG);//发送
R2=R1&0x0F;//赋值第四位
LCD_WRITE_4(R2,FLAG);//发送
DELAY_US(10);//必要延迟
}
void LCD_WriteStr(char line,char *s ){
char i;
LCD_WRITE(line,COM);
for(i=0;i<16;i++)
LCD_WRITE(s[i],DATA); //写16个数据,即显示1行
}
//===============================================
//延时(n)ms
void DELAY_MS(unsigned int n)
{ unsigned int j;
char k;
for(j=0;j<n;j++)
for(k=108;k>0;k--)
NOP();
}
//===============================================
//延时(n*10)us+12us,精确,包括调用与返回时间
void DELAY_US(char n)
{
char j;
j=n;
while(j>0)
{ j--;
NOP();NOP();NOP();NOP();
}
}
下面是.h文件,上面是.c文件
#ifndef __LCD_H
#define __LCD_H
#include <xc.h>
#define LCD_E RD6 //使能,下降沿有效
#define LCD_RW RD5 //读写功能引脚;1:读;0:写
#define LCD_RS RD4 //命令和数据选择端 1:数据;0:命令
#define COM 0
#define DATA 1
#define LINE1 0b10000000
#define LINE2 0b11000000
extern char DD[16],CC[16],MM[16],GG[16],CK[16];
char DD[16]={"**SG Generator**"},
CC[16]={" Author: Gritty "},
MM[16]={"wave "},
GG[16]={"A=5.00V**F=100HZ"},
CK[16]={"A=5.00V**F=100HZ"};
void LCD_Init(void);
void LCD_WRITE_4(char R1,char FLAG);
char LCD_READ(void);
void LCD_BUSY(void);
void LCD_WRITE(char R1,char FLAG);
void DISP_C(char);
void LCD_WriteStr(char line,char *s );
void DELAY_MS(unsigned int n);
void DELAY_US(char n);
void Disp_fre_Am(unsigned int frequence,unsigned int voltage,unsigned char sg);
#endif
串口通讯技术
串口通讯协议较为简单,其由RX,TX和GND实现通讯,其可以通过TTL转USB实现与上位机通讯,也可以通过一些其他的转换协议,实现与上位机通讯,其是最基本的通讯技术。
#include "usart.h"
#define _XTAL_FREQ 4000000
void USART_Initial(void){
TRISC=0B10111111; //定义端口输出输入情况
RCIE=1;//串行通信接收中断使能
SPBRG=25;
TXSTA=0B01100100;
RCSTA=0B11010000;
}
void USART_Send(char a){
TXREG=a;
while(TRMT==0);
}
下面是.h文件,上面是.c文件
#ifndef __USART_H
#define __USART_H
#include <xc.h>
//#define TX1 RC6
//#define RX1 RC7
void USART_Initial(void);
void USART_Send(char a);
#endif
波形采样技术
通过MATLAB可以生成采样的波形,得到采样的点数,MATLAB代码较为简单,可以自己书写。
生成的点数文件如下
#ifndef __WAVE_H
#define __WAVE_H
const char sin[44]={
128,146,164,181,197,212,225,236,244,251,255,255,255,251,244,236,225,212,197,181,164,146,128,110,92,75,59,44,31,20,12,5,1,0,1,5,12,20,31,44,59,75,92,110
};
const char saw[64]={
0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,
164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252
};
const char sin_high[28]={
128,156,184,208,228,243,253,255,253,243,228,208,184,156,128,100,72,48,28,13,3,0,3,13,28,48,72,100
};
const char saw_high[32]={
0,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248
};
#endif /* XC_HEADER_TEMPLATE_H */
核心技术
- 核心技术为有限状态机
FSM,利用FSM实现单片机在不同状态的切换。在不同的状态下,单片机处于不同的状态;利用判断条件,实现单片机在不同的状态下的跃迁; - 在输出调节中采用数值逼近技术,利用线性逼近实现定时器时间与输出频率之间的准确响应;
- 总纲: 程序经过初始化后就进入 while 循环或中断两种工作状态。在 while 循环状态中,主要是处理复杂的需要较大运算量的工作。这就包括显示信息、频率控制、控制输出和旋钮调频操作。这些操作都需要一定的运算量,具有一定的时间复杂度。在中断工作下,主要完成 I/O 口电平检测、波形输出和串口数据接收。这些操作需要的时间很短,I/O 口电平检测和串口数据接收后会关闭中断功能,退出中断,回到主函数中,完成更新状态的操作。程序功能主体由波形发生、频率控制、显示信息、控制处理和旋钮调频组成;
- 波形发生: 通过定时器 2 中断,每隔一段时间通过 IIC 通讯向 PCF8591 发送波形数组中的一个数据,PCF8591 将数据处理后输出电平信号。只要保证输出数组的大小合适且发送时间间隔不太大,在 PCF8591 输出就可以得到接近需要的波形信号,后级电路经过滤波就可以得到更加符合要求的波形信号;
- 频率控制: 频率控制主要用到的定时器 2 的定时功能。定时器 2 初始化后,通过改变定时器2的PR2寄存器,改变发送数据之间的时间间隔,实现改变频率。显示信息:通过四线制工作方式控制 LCD1602,在频率、赋值和波形类型发送变化的时候更新液晶显示屏上的信息;
- 控制处理: 1.按钮控制:控制处理中用到了三个按钮,分别对应功能是模式切换、频率刷新、串口刷新。按钮触发通过 RB 口中断实现,利用边沿触发模式,实现对 RB 口电平变化的检测。模式切换利用对 RB0 口触发次数的计数。模式共有 4 种,分别是固定频率输出正弦、固定频率输出锯齿波、变化频率输出正弦波和变化频率输出锯齿波。通过对触发次数计数,每次触发次数储存在 count_wave(模式状态的标准位)中;
- 频率刷新是利用 RB1 口电平中断,一旦触发中断就就改变全局变量 count_frequence(刷新频率标志位)的值;串口刷新也是利用 RB2口电平中断,一旦触发该中断就改变 count_usart(刷新串口标志位);
- 串口控制: 当串口中断触发后,将数据接收后存在全局变量的数组中;当发生按钮控制的信号触发后,就将中断总开关关闭,使其退出中断,回到主函数当中。在主函数的 while 循环中,根据标志位进行相对应操作;
- 旋钮调频: 当旋钮变化后,按下控制处理 RB2 按钮,count_frequence 标志位置 1,退出中断。在主函数 while 循环中,count_frequence=1 进入频率更新模式,AD 采样一次后,通过频率与 ADC 采样结果对应函数关系改变 PR2 值,实现对输出波形频率的改变。