作者:Whappy
时间:2024.9.20
总结一下!基础实验到这儿里就圆满结束,历经25天,将51单片机学完并亲自手敲代码近5000行,在手敲代码过程中,明显感觉的看和敲,明显就是不同的感觉,创作不易,在原有的代码上加上自己的想法并加以实现!接下向STM32发起进攻,开始学习!
介绍:
硬件电路
针对于上图两个PNP的三极管主要用来调制的,是因为自然界中有很多红外光,我们想要得到我们需要的的红外光,就要进行调制,通过一个38hz的频率进行调制,因为自然界的红外光不可能以38hz的频率跳动,后期通过滤波器将我们需要的波形提取来就可以了!
还有红外接收头,OUT引脚接收的脉冲特别快,需要我们及时处理,所以,我们采取51单片机的外部中断来对这个脉冲进行处理!
发送和接收装置
通过上述解释:这个高电平和低电平不是我们平常说的一个周期内的高电平,而是一个周期中高低低电平所持续的时间,同样,低电平也是,看上图!
NEC编码
示波器采集的按键波形变化,还是地址码+地址反码+命令字+命令字反码
实物外观
可以看出要控制器所对应的键码值!
51单片机的外部中断
这个外部中断,也是我们常用的一种处理手段,外部中断也比较简单,只需要打开相关寄存器,配置一下外部中断服务函数即可!如下代码
void INT0_Init(void) //打开外部中断相关的的寄存器(寄存器可单独配置)
{IT0 = 1; //配置位低电平触发模式IE0 = 0; //中断标志位EX0 = 1; //外部中断0使能EA = 1; //中断总使能PX0 = 1; //中断最高优先级
}/*
//外部中断0 服务函数
void Int0_Routine() interrupt 0
{Number++;
}
*///注:配置完成之后就可以在主函数中初始化了,在主函数下面加上我们的中断服务函数,即可触发中断
实验一:红外遥控实验(其余代码参考往期实验)
这个程序是使用红外遥控器控制数值的显示,并在LCD1602上输出。主要功能是通过红外接收数据,显示遥控器的地址码、命令码,并且根据接收到的命令对变量 Num
进行增减。
主要流程:
-
LCD 初始化:
LCD_Init()
初始化 LCD1602 显示屏。- 在第一行显示 "ADDR CMD NUM" 作为标题。
- 在第二行初始化显示地址码、命令码和变量
Num
的值。
-
红外接收初始化:
IR_Init()
初始化红外接收功能,准备接收遥控器信号。
-
循环处理红外信号:
IR_GetDataFlag()
和IR_GetRepeatFlag()
判断是否接收到数据帧或连发帧。- 如果接收到信号:
- 调用
IR_GetAddress()
获取遥控器地址码并显示在LCD上。 - 调用
IR_GetCommand()
获取遥控器命令码并显示在LCD上。
- 调用
-
处理命令:
- 如果接收到的命令码是
IR_VOL_MINUS
(对应遥控器的音量减键),变量Num
自减。 - 如果接收到的命令码是
IR_VOL_ADD
(对应遥控器的音量加键),变量Num
自增。
- 如果接收到的命令码是
-
显示数值:
- 更新LCD,显示变量
Num
的值。
- 更新LCD,显示变量
通过遥控器上的音量加减键,用户可以控制数值 Num
的增减,并实时在LCD上看到变化。
main.c
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "IR.h"unsigned char Num;
unsigned char Address;
unsigned char Command;void main()
{LCD_Init();LCD_ShowString(1,1,"ADDR CMD NUM");LCD_ShowString(2,1,"00 00 000");IR_Init();while(1){if(IR_GetDataFlag() || IR_GetRepeatFlag()) //如果收到数据帧或者收到连发帧{Address=IR_GetAddress(); //获取遥控器地址码Command=IR_GetCommand(); //获取遥控器命令码LCD_ShowHexNum(2,1,Address,2); //显示遥控器地址码LCD_ShowHexNum(2,7,Command,2); //显示遥控器命令码if(Command==IR_VOL_MINUS) //如果遥控器VOL-按键按下{Num--; //Num自减}if(Command==IR_VOL_ADD) //如果遥控器VOL+按键按下{Num++; //Num自增}LCD_ShowNum(2,12,Num,3); //显示Num}}
}
Timer0.c
#include <REGX52.H>// 定时器0初始化函数,设置定时器0为模式1(16位定时器模式),每1毫秒计时一次,工作在11.0592MHz晶振下
void Timer0_Init()
{TMOD &= 0xF0; // 清除定时器0的模式位(低4位),保留高4位(定时器1的设置)TMOD |= 0x01; // 设置定时器0为模式1(16位定时器)TL0 = 0; // 设置定时器0低8位初值为0TH0 = 0; // 设置定时器0高8位初值为0TF0 = 0; // 清除定时器0的溢出标志位(TF0)TR0 = 0; // 关闭定时器0的计时
}// 设置定时器0计数器值
void Timer0_SetCounter(unsigned int Value)
{TH0 = Value / 256; // 将高8位写入TH0寄存器TL0 = Value % 256; // 将低8位写入TL0寄存器
}// 获取定时器0当前的计数值
unsigned int Timer0_GetCounter(void)
{return (TH0 << 8) | TL0; // 将TH0和TL0的值组合成16位数并返回
}// 启动或停止定时器0的计时功能
// 参数:Flag为1时启动定时器,Flag为0时停止定时器
void Timer0_Run(unsigned char Flag)
{TR0 = Flag; // 根据Flag设置TR0位,1表示启动定时器0,0表示停止定时器0
}
注释说明:
- Timer0_Init():初始化定时器0为模式1(16位定时器模式),并设置初值。此模式下,定时器从
TH0|TL0
组合的16位计数器开始计数,直到溢出。 - Timer0_SetCounter():根据传入的
Value
值,设置定时器的当前计数值。 - Timer0_GetCounter():获取当前定时器的计数值(16位数),将高8位和低8位组合返回。
- Timer0_Run():启动或停止定时器的计时功能,通过
Flag
参数来控制。
INT0.c
#include <REGX52.H>// 外部中断0初始化函数
void INT0_Init(void)
{IT0 = 1; // 设置外部中断0为下降沿触发模式(1为下降沿触发,0为低电平触发)IE0 = 0; // 清除外部中断0的中断标志位EX0 = 1; // 使能外部中断0EA = 1; // 全局中断使能PX0 = 1; // 设置外部中断0为高优先级(1为高优先级,0为低优先级)
}/*
// 外部中断0的中断服务函数
void Int0_Routine() interrupt 0
{Number++; // 每次触发外部中断0时,将变量 Number 自增
}
- INT0_Init():初始化外部中断0,将其配置为下降沿触发,并使能相关中断。通过设置中断优先级位来设置其为高优先级中断。
IT0 = 1
:配置外部中断0为下降沿触发。IE0 = 0
:清除中断标志位,确保没有待处理的中断。EX0 = 1
:使能外部中断0。EA = 1
:开启全局中断,使单片机响应中断。PX0 = 1
:设置外部中断0为高优先级。
- Int0_Routine():这是外部中断0的中断服务函数,执行中断时该函数会被调用。在该例子中,每次外部中断触发时,变量
Number
会自增。
注:中断服务函数 Int0_Routine()
被注释掉,可以根据需要进行启用。
IR.c (核心代码:主要处理红外遥控接收和发送数据)
#include <REGX52.H>
#include "Timer0.h"
#include "INT0.h"unsigned int IR_Time; // 用于记录红外信号的时间间隔
unsigned char IR_State; // 用于记录红外接收状态机的状态
unsigned char IR_Data[4]; // 用于存储接收到的红外数据信息
unsigned char IR_pData; // 用于记录当前接收数据的位索引unsigned char IR_DataFlag; // 用于标记是否接收到数据帧
unsigned char IR_RepeatFlag; // 用于标记是否接收到连发信号
unsigned char IR_Address; // 存储接收到的红外遥控器地址
unsigned char IR_Command; // 存储接收到的红外遥控器命令// 红外遥控初始化函数
void IR_Init(void)
{INT0_Init(); // 初始化外部中断0,用于捕获红外接收信号的下降沿Timer0_Init(); // 初始化定时器0,用于计时
}// 获取数据帧标志位的函数
unsigned char IR_GetDataFlag(void)
{if(IR_DataFlag) // 如果收到数据帧标志置位{IR_DataFlag = 0; // 清除数据帧标志return 1; // 返回1表示已接收到数据帧}return 0; // 否则返回0
}// 获取连发信号标志位的函数
unsigned char IR_GetRepeatFlag(void)
{if(IR_RepeatFlag) // 如果收到连发信号标志置位{IR_RepeatFlag = 0; // 清除连发信号标志return 1; // 返回1表示已接收到连发信号}return 0; // 否则返回0
}// 获取接收到的红外遥控地址
unsigned char IR_GetAddress(void)
{return IR_Address;
}// 获取接收到的红外遥控命令
unsigned char IR_GetCommand(void)
{return IR_Command;
}// 外部中断0服务函数,处理红外接收的信号
void Int0_Routine() interrupt 0
{// 状态0:空闲状态,检测到信号开始,重置计时器if(IR_State == 0) {Timer0_SetCounter(0); // 将定时器计数器清零Timer0_Run(1); // 启动定时器IR_State = 1; // 设置状态为1,等待信号}// 状态1:等待Start信号或Repeat信号else if(IR_State == 1) {IR_Time = Timer0_GetCounter(); // 获取上次中断到此次中断的时间Timer0_SetCounter(0); // 计时器清零准备下一次测量// 检测到Start信号if(IR_Time > (12442-500) && IR_Time < (12442+500)){IR_State = 2; // 转换到状态2,准备接收数据}// 检测到Repeat信号else if(IR_Time > (10368-500) && IR_Time < (10368+500)){IR_RepeatFlag = 1; // 置连发信号标志Timer0_Run(0); // 停止定时器IR_State = 0; // 返回空闲状态}else{IR_State = 1; // 未收到有效信号,保持状态1}}// 状态2:接收数据else if(IR_State == 2) {IR_Time = Timer0_GetCounter(); // 获取时间间隔Timer0_SetCounter(0); // 计时器清零// 判断是逻辑0还是逻辑1if(IR_Time > (1032-500) && IR_Time < (1032+500)){IR_Data[IR_pData/8] &= ~(0x01 << (IR_pData % 8)); // 写入逻辑0IR_pData++; // 数据位索引加1}else if(IR_Time > (2074-500) && IR_Time < (2074+500)){IR_Data[IR_pData/8] |= (0x01 << (IR_pData % 8)); // 写入逻辑1IR_pData++; // 数据位索引加1}else{IR_pData = 0; // 出现错误,数据位清零,返回状态1IR_State = 1;}// 如果接收到完整的32位数据if(IR_pData >= 32){IR_pData = 0;// 检查数据的有效性if((IR_Data[0] == ~IR_Data[1]) && (IR_Data[2] == ~IR_Data[3])){IR_Address = IR_Data[0]; // 提取地址码IR_Command = IR_Data[2]; // 提取命令码IR_DataFlag = 1; // 置数据帧标志位}Timer0_Run(0); // 停止定时器IR_State = 0; // 返回空闲状态}}
}
代码思路
-
初始化部分:
-
调用
IR_Init()
函数来初始化外部中断0和定时器0,确保红外接收能够计时和响应信号的变化。
-
-
状态机控制:
-
通过
IR_State
进行状态管理。程序根据中断触发的时间判断信号类型(启动信号、连发信号或数据)。 -
根据不同状态,程序分别执行启动计时、判断信号类型、接收并解码红外数据的操作。
-
-
数据接收和处理:
-
红外信号的编码通过时间间隔来区分逻辑0和逻辑1。程序根据红外信号的时间长短判断数据位,最终解码成地址和命令。
-
采用位操作将红外信号按位存入
IR_Data
数组,并在接收到完整32位数据后校验数据的有效性(利用地址和命令码的互补关系)。
-
-
标志位与状态管理:
-
使用
IR_DataFlag
和IR_RepeatFlag
来标记是否接收到完整数据帧或连发信号。 -
外部模块可以通过
IR_GetDataFlag()
等函数获取这些标志位,并作出相应的处理。
-
函数调用关系
-
IR_Init()
初始化中断和定时器。 -
Int0_Routine()
是外部中断服务函数,负责处理红外信号,状态机在这个函数中运行。 -
IR_GetDataFlag()
和IR_GetRepeatFlag()
函数提供了对外的接口,用于其他模块判断红外遥控信号状态。 -
IR_GetAddress()
和IR_GetCommand()
函数用于获取解码后的红外遥控地址和命令。
目的和好处
-
状态机的设计:通过
IR_State
实现多阶段处理红外信号(如空闲、判断信号、数据接收),结构清晰,便于调试与扩展。 -
标志位管理:通过
IR_DataFlag
和IR_RepeatFlag
等标志位,模块化地提供状态信息,便于其他模块获取数据而不直接干扰中断逻辑。 -
位操作与定时器结合:通过定时器捕获时间差,结合位操作解码数据,使得红外信号的处理精确而高效。
思想方法
-
模块化设计:将不同功能(如定时器、外部中断、信号解码)分离为不同的函数,方便维护和扩展。
-
状态机模型:通过有限状态机的方式处理信号,能够清晰管理不同阶段的任务,避免逻辑混乱。
-
时间敏感的处理:通过定时器精确测量时间,结合中断机制,高效地接收和处理红外信号。
实验二:红外遥控控制电机调速实验(其余代码往期实验)
目的是通过红外遥控器来控制电机的转速,并通过数码管显示当前速度值。具体功能如下:
-
红外遥控输入:
-
程序接收红外遥控器的命令码,根据不同的按键(如
IR_0
,IR_1
,IR_2
,IR_3
)来设置不同的电机速度。
-
-
电机速度控制:
-
程序根据遥控器的命令,通过
Motor_SetSpeed()
函数来调整电机的实际转速。设定的速度分为4个级别:停止、50%、75%、100%。
-
-
数码管显示:
-
使用数码管
Nixie()
来显示当前的速度状态,将当前的速度级别(0、1、2、3)显示在数码管上。
-
总体思想
-
模块化设计:
-
程序分为几个独立模块,如
Motor_Init()
初始化电机,IR_Init()
初始化红外接收,Motor_SetSpeed()
设置电机转速,Nixie()
用于显示当前速度值。这样的设计便于调试和扩展。
-
-
红外控制:
-
利用红外接收模块,通过接收遥控器发送的命令码,改变电机的速度。程序通过不断检查是否有数据帧到来,一旦收到有效数据,就解析命令码并做出响应。
-
-
状态机思想:
-
代码使用简单的状态判断(速度级别为0到3),根据不同的命令调整电机的运行状态,并通过显示屏实时反馈当前状态。这种逻辑使得代码结构清晰,处理不同输入时能够快速响应。
-
-
解耦合和灵活性:
-
将电机控制和遥控器输入解耦合,遥控器只需发出简单的命令,电机部分负责实现速度控制。通过简单扩展,系统可以支持更多的命令和功能,例如控制电机的方向或其他附加功能。
-
这样做的好处
-
可扩展性:功能可以方便地扩展,比如增加更多速度级别或其他控制命令,代码结构简单易懂,模块化设计便于维护。
-
用户友好:通过遥控器方便地控制电机运行,数码管实时显示当前状态,用户能直观地知道电机运行情况。
-
高效运行:通过红外信号触发的状态机结构,使得程序在低资源消耗下响应快速。
main.c
void main()
{Motor_Init(); // 电机初始化IR_Init(); // 红外遥控初始化while(1) // 主循环{if(IR_GetDataFlag()) // 如果接收到红外遥控的有效数据帧{Command = IR_GetCommand(); // 获取遥控器发来的命令码// 根据不同的遥控命令码设置速度值if(Command == IR_0) { Speed = 0; } // 命令码为 0,速度设为 0if(Command == IR_1) { Speed = 1; } // 命令码为 1,速度设为 1if(Command == IR_2) { Speed = 2; } // 命令码为 2,速度设为 2if(Command == IR_3) { Speed = 3; } // 命令码为 3,速度设为 3// 根据速度值调整电机的实际转速if(Speed == 0) { Motor_SetSpeed(0); } // 速度0,电机停止if(Speed == 1) { Motor_SetSpeed(50); } // 速度1,电机以50%的功率运行if(Speed == 2) { Motor_SetSpeed(75); } // 速度2,电机以75%的功率运行if(Speed == 3) { Motor_SetSpeed(100); } // 速度3,电机以100%的功率运行}Nixie(1, Speed); // 数码管显示当前速度值}
}
IR.c(同上)
INT0.c(同上)
Timer0.c(同上)
Timer1.c
#include <REGX52.H>// 定时器1初始化函数,用于配置定时器1,产生一个100微秒的定时中断。
/**
* @brief 定时器初始化(51单片机软件内置配置的定时器)
* @param 无
* @retval 无
*/
void Timer1_Init() //100微秒@11.0592MHz
{TMOD &= 0x0F; // 清除定时器1的模式位,保持定时器0的模式不变TMOD |= 0x10; // 设置定时器1为模式1,即16位定时模式TL0 = 0xA4; // 设置定时器1的低位初值为0xA4TH0 = 0xFF; // 设置定时器1的高位初值为0xFFTF0 = 0; // 清除定时器1的溢出标志位TR0 = 1; // 启动定时器1// 启用定时器中断ET0 = 1; // 打开定时器1中断EA = 1; // 打开总中断PT0 = 0; // 设置定时器1中断优先级为低优先级
}/* 定时器中断函数模板 */
/*
void Timer0_Rountine(void) interrupt 3
{static unsigned int T0Count; // 用于记录定时器0的计数TL0 = 0x66; // 设置定时初值TH0 = 0xFC; // 设置定时初值T0Count++; // 每次中断发生时自增1if(T0Count >= 1000) // 1000次中断(即1秒)后执行翻转P2_0口{T0Count = 0;P2_0 = ~P2_0; // 反转P2_0引脚的输出电平,达到控制外部设备的效果}
}
*/
Motor.c
#include <REGX52.H>
#include "Timer1.h"sbit Motor = P1^0; // 将 P1 口的第0位定义为 Motor 引脚,用于控制电机的开关unsigned char Counter, Compare; // Counter 用于计时,Compare 用于占空比比较/*** @brief 电机初始化函数,初始化定时器1* @param 无* @retval 无*/
void Motor_Init()
{Timer1_Init(); // 调用定时器1初始化函数,开始计时
}/*** @brief 设置电机转速* @param Speed 速度参数,范围为0-100,用于调节电机的占空比* @retval 无*/
void Motor_SetSpeed(unsigned char Speed)
{Compare = Speed; // 将传入的速度值赋给 Compare,作为占空比参考值
}/*** @brief 定时器1中断服务函数* 使用定时器实现 PWM 调速功能* @param 无* @retval 无*/
void Timer1_Rountine(void) interrupt 3 // 定时器1中断函数
{TL0 = 0xA4; // 设置定时器初值,确保每次中断后保持相同的定时时间TH0 = 0xFF; // 设置定时器高位初值Counter++; // 每次中断时,计时器 Counter 自增1Counter %= 100; // 将 Counter 限制在 0 到 99 的范围内,形成 100 个周期(模拟 PWM 占空比)// 比较 Counter 和 Compare 的值,用于控制占空比if(Counter < Compare){Motor = 1; // 如果 Counter 小于 Compare,电机引脚输出高电平,电机通电工作}else{Motor = 0; // 如果 Counter 大于或等于 Compare,电机引脚输出低电平,电机停止工作}
}
代码说明
-
Motor_Init()
:-
初始化电机控制模块,内部调用了
Timer1_Init()
函数,启动定时器1以实现电机的速度控制。
-
-
Motor_SetSpeed()
:-
该函数用于设置电机的转速。传入的
Speed
参数(范围0-100)表示电机速度的占空比,控制电机开启的时间占整个周期的比例。
-
-
Timer1_Rountine()
:-
定时器1的中断服务函数,用于实现电机的PWM(脉宽调制)控制。
-
定时器每触发一次中断,
Counter
自增并与设定的占空比Compare
进行比较:-
如果
Counter
小于Compare
,电机引脚输出高电平(电机运行)。 -
如果
Counter
大于或等于Compare
,电机引脚输出低电平(电机停止)。
-
-
通过这种方式,程序控制了电机开启与关闭的比例,从而调整电机的转速。
-
总体思想
通过定时器中断实现 PWM 控制,以调节电机的工作状态。Counter
循环自增形成一个周期,并根据占空比 Compare
控制电机的开关,从而实现电机的转速控制。
最后总结
通过C语言编程控制51单片机的几种常见功能,包括红外遥控解码、电机转速控制、定时器中断和外部中断等。这些功能的核心思想是利用单片机的定时器、中断机制和外设来实现对外部设备的控制和数据采集。以下是关键点的总结:
-
红外遥控解码:
-
通过外部中断
INT0
捕获红外信号的下降沿,用定时器测量信号脉冲宽度,从而解码出遥控器的地址和命令。 -
利用状态机进行红外信号的解析,包括起始信号、数据位解码和重复信号处理。
-
解码后的命令可以用于控制电机或其他设备的操作。
-
-
定时器的使用:
-
定时器用于生成精准的时间延迟、控制设备(如电机)、或周期性执行任务。
-
在定时器中断服务函数中,计时器
Counter
和占空比Compare
的比较决定了电机的开关状态,从而实现 PWM 调速。
-
-
PWM 控制电机:
-
通过定时器中断产生的PWM信号控制电机的转速。利用占空比来控制电机工作时间的比例,从而调整电机的转速。
-
电机控制分为初始化、设置速度和根据设定的速度输出对应的PWM信号,形成灵活的调速机制。
-
-
外部中断:
-
外部中断用于响应外部事件,例如红外遥控信号的输入。中断服务函数快速响应并执行特定任务,确保即使主程序忙碌也能处理紧急事件。
-
-
思想方法:
-
模块化设计:通过封装函数实现功能模块的独立性,如红外解码、定时器配置、电机控制等。这种设计思路使代码具有良好的扩展性和维护性。
-
状态机:红外解码使用状态机方法,根据接收信号的不同状态(如空闲、等待、解码等)做出相应的处理,确保程序运行稳定。
-
中断优先机制:中断机制保证了单片机可以及时响应重要事件,如定时器溢出或外部信号输入,而不会影响主程序的执行。
-
优点和好处:
-
实时性强:中断机制保证了高优先级任务(如定时和外部输入)的及时响应,不会因为主程序的延迟而错失事件。
-
精确控制:利用定时器实现精准的时间控制,结合PWM调制实现了对电机速度的精确调节。
-
模块化和可扩展性:各个功能模块相互独立,便于后期扩展新功能或修改现有功能。
通过这些机制和设计,代码能够高效地管理外部设备和信号输入,实现自动化控制和调节功能。