1.PWM
用一个定时器的实现PWM的方法。以周期为1ms(1kHZ)为例,要产生其它频率的PWM波,程序中只需作简单修改即可。用一个定时器时(如定时器T0),首先要确定PWM的周期T和占空比D,确定了这些以后,就可以用定时器产生一个时间基准t,比如定时器溢出n次的时间是PWM的高电平的时间,则DT=nt,类似的可以求出PWM低电平时间需要多少个时间基准n。
因为这里我们是产生周期为1ms(1kHZ)的PWM,所以可设置中断的时间基准为0.01ms,,然后中断100次即为1ms。在中断子程序内,可设置一个变量如time,在中断子程序内,有三条重要的语句:
1、当time>=100时,time清零(此语句保证频率为1kHZ);
2、当time>n时(n应该在0-100之间变化开),让单片相应的I/O口输出低电平;
3、当time<=n时,让单片相应的I/O口输出高电平,此时占空比就为%n。
所以基本上就是设一个基准,调整占空比和频率就好了。
sbit PWM=P2^0;// P2.0输出pwm
uchar time; // 定义占空比的变量
void tim0() interrupt 1
{
TR0=0;//赋初值时,关闭定时器
TH0=0xff;//(65536-10)/256;//赋初值定时
TL0=0xf7;//(65536-10)%256;//0.01ms
TR0=1;//打开定时器
time++;
if(time>=100) //1khz
time=0;
if(time<=30) //占空比%30,可改
PWM=1;
else PWM=0;
}
如果要使占空比可调,比如做一个呼吸灯,就要多一些基准变量
全部代码(之前的不是标准的呼吸灯,改了之后好多了)
#include "STC12C5A60S2.H"
typedef unsigned char uchar;
typedef unsigned int uint;
/*10Khz --- 100us----10ms更新一次pwm_n*/
int pwm_conut =0;//实时次数
int pwm_n = 0;//pwm的辉度
int pwm_n_time =0;//PWM辉度时间
bit turn_flag = 1;//方向标志位
void Timer0Init();
void All_Init()
{
P2 = ((P2&0X1F)|0X80);
P0 = 0XFF;
P2 = ((P2&0X1F)|0XA0);
P0 = 0X00;
P2 = ((P2&0X1F)|0XC0);
P0 = 0X00;
P2 = ((P2&0X1F)|0XE0);
P0 = 0XFF;
}
void main(void)
{
All_Init();
Timer0Init();
while (1)
{
if(pwm_conut> pwm_n)
{
P2 = (P2&0X1F|0X80);
P0 = 0XFE;
}
else
{
P2 = (P2&0X1F|0X80);
P0 = 0XFF;
}
}
}
void Timer0Init(void) //1微秒@12.000MHz
{
AUXR |= 0x80; //定时器时钟1T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0xF4; //设置定时初值
TH0 = 0xFF; //设置定时初值
EA = 1;
ET0 = 1;
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
}
void Time0Inter() interrupt 1//1us
{
if(++pwm_conut == 100)//100us为一个周期
{
pwm_conut = 0;
}
if(++pwm_n_time == 10000)//10ms更新一次pwm_n
{
pwm_n_time = 0;
if(turn_flag)//如果为正向
{
pwm_n+= 10;
if(pwm_n == 100)
{
turn_flag =0;
}
}
else//如果为负向
{
pwm_n -= 10;
if(pwm_n == 20)//不直接灭了,不好看
{
turn_flag =1;
}
}
}
}
接下来是对I2C通信的一个学习,包括
·EEPROM存储芯片AT24C02的使用
·AD/DA转换芯片PCF8591的使用
·I2C通信的应用
2.EEPROM
一.基础了解
EPROM (Electrically Erasable Programmable Read-Only Memory),带电可擦可编程只读存储器–一种掉电后数据不丢失的存储芯片。 EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般即插即用。(lqb板子上就是AT24C02芯片)。一根为SCL时钟信号线,另一根为SDA数据信号线(传输数据和地址也包括开始结束和ACK信号)这两根线必须接10k的上拉电阻。所以一开始SCL,SDA都是高电平。这两根线与EEPROM的SCL,SDA线相连。iic总线每次传输8bit(一个字节)的数据。而且iic总线是串行主从结构,读和写都由主机操作,从机只能接受。
24C02中有一个IP地址,它的目的就是为了iic总线能识别挂在总线上的从机。因为IIC总线可以挂载不止一个的设备。
根据24c04的数据手册可知,它有7位地址,还有一位读/写位(R/W)。1为读操作0为写操作。
但是24C04的前四位被厂家固化为1010,所以芯片管脚上只剩3个地址管脚(A0,A1,A2)。
24C04它还有一位写保护位WP(write protect)。一般接低电平(接地),用于可读可写。
A0 A1 A3如果都接地,那么24C04的地址为1010 000,也就是0xA1/0XA0
A0 A1 A3如果接高电平,那么从机设备地址为1010 111,也就是0xAF
要对EEPROM的内容进行修改,主要用到下面的程序:
①读取
②写入
③删除扇区
二、EEPROM单字节读写操作时序
1、EEPROM写数据流程
(1)第一步,首先写IIC的起始信号,紧接着写上首字节,即我们前边讲的IIC的器件地址,并且在读写方向上选择“写”操作。
(2)第二步,发送数据的存储地址。例如24C02芯片一共有256个字节的存储空间,地址从0x00~0xFF,我们想把数据存储在芯片的哪个位置,此刻写入的就是对应的那个地址。
(3)第三步,发送要存储的数据第一个字节、第二个字节……,注意在写数据的过程中,EEPROM每个字节都会回应一个“应答位0”,来告诉我们在EEPROM芯片中写入数据成功,如果没有回应答位,说明写入数据不成功。
在写数据的过程中,每成功写入一个字节数据,EEPROM芯片的存储地址就会自动加1,当加到0xFF后,再写一个字节数据,地址就会溢出又变成了0x00。
2、EEPROM读数据流程
(1)第一步,首先写IIC的起始信号,紧接着写上首字节,即我们前边讲的IIC的器件地址,并且在读写方向上选择“写”操作。
这个地方可能有人会感到很诧异,我们明明是读数据为何方向也要选“写”呢?刚才说过了,24C02芯片一共有256个地址,我们选择写操作,是为了把所要读的数据的存储地址先写进去,告诉EEPROM我们要读取哪个地址的数据。这就如同我们打电话,先拨总机号码(EEPROM的器件地址),而后还要继续拨分机号码(EEPROM的数据地址),而拨分机号码这个动作,主机仍然是发送方,方向依然是“写”。
(2)第二步,发送要读取的数据的地址,注意是地址而非存储在EEPROM中的数据,通知EEPROM我要读取哪个分机的信息。
(3)第三步,重新发送IIC的起始信号和器件地址,并且在读写方向位上选择“读”操作。
这三步当中,每一个字节实际上都是在“写”,所以每一个字节EEPROM都会回应一个“应答位0”。
(4)第四步,读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位ACK(0)”,如果不想读了,告诉EEPROM,我不想要数据了,别再发数据了,那就发送一个“非应答位NAK(1)”。
和写操作规则一样,每成功读取一个字节数据,EEPROM芯片的存储地址就会自动加1,那如果我们想继续往下读,给EEPROM一个ACK(0)低电平,然后再继续给SCL时钟线完整的时序,EEPROM会继续往外送数据。如果我们不想读了,要告诉EEPROM不要数据了,那我们直接给一个NAK(1)高电平即可。这个地方大家要从逻辑上理解透彻,不能简单的靠死记硬背了,一定要理解明白。
3、总结
(1)在通常的EEPROM应用中,单片机是主机,24C02是从机。
(2)无论是读操作还是写操作,SCL时钟线始终都是由主机控制的。
(3)写数据的时候应答信号由从机给出,表示从机是否正确接收了数据。
(4)读数据的时候应答信号则由主机给出,表示是否继续读下去。
部分代码块
void Write_eeprom(uchar addr,uchar val);
uchar Read_eeprom(uchar addr);
void Display(uint dat);
void main(void)
{
uchar reset_cnt = 0; //开机次数存储 (最大存储值255)
All_Init();
reset_cnt = Read_eeprom(0x00); //从AT24C02地址0x00中读取数据
reset_cnt++; //掉电不消失
Write_eeprom(0x00,reset_cnt); //向AT24C02地址0x00中写入数据 最大256
while(1)
{
Display(reset_cnt);
}
}
void Display(uint dat)
{
uchar i,j = 0;
uchar str[3];
uchar d = dat;
for (i = 0; i < 3; i++)
{
str[i] = d % 10;
d /= 10; //不断取出个位的数
}
for(j = 0; j<3;j++)//要显示几位就循几次
{
P2 = (P2&0X1F|0XC0);//位
P0 = Wei_Data[7-j];
P2= (P2&0X1F|0XE0);//段
P0 = Duan_Data[str[j]];
Delay_ms(3);
P0 = 0XFF;//消影
}
}
void Write_eeprom(uchar addr,uchar val)
{
IIC_Start();//总线开始
IIC_SendByte(0xa0);//iic发送起始地址
IIC_WaitAck();
IIC_SendByte(addr);//发送寄存器地址(写)
IIC_WaitAck();
IIC_SendByte(val);//数据
IIC_WaitAck();
IIC_Stop();//总线结束
Delay_ms(10);//延迟
}
uchar Read_eeprom(uchar addr)
{
uchar da;
IIC_Start();
IIC_SendByte(0xa0); //发送IIC起始地址 也可以理解为写命令
IIC_WaitAck(); //等待ACK应答
IIC_SendByte(addr); //发送寄存器地址
IIC_WaitAck();
IIC_Start(); //重启IIC总线
IIC_SendByte(0xa1); //发送IIC起始地址 + 1表示读 发送读命令
IIC_WaitAck();
da = IIC_RecByte();
IIC_Ack(0); //发送ACK应答 不读了
IIC_Stop();
return da;
}
总之根据figure8,11就可以利用I2C的驱动函数自己写出读写字节的函数。