江协科技/江科大-51单片机入门教程——P[5-1] 模块化编程 & P[5-2] LCD1602调试工具

在本章节,将讲解两个对于这套教程非常重要的知识点,一是模块化编程,二是 LCD 调试工具。

目录

一、模块化编程

二、LCD1602调试工具


一、模块化编程

模块化编程是一种分类管理的思想,把不同模块功能的实现代码放在不同文件里,再集中组织起来,可提高代码的可阅读性、可维护性和可移植性。LCD 调试工具基于模块化编程,既是模块化的实例,又为调试代码提供极大方便,它提供类似 C 语言中 printf 函数的功能,在调试时若没有界面反馈内部数字状态和计算结果,调试将如同盲人过河。教程中已写好 LCD1602 液晶屏的代码,目的是在刚开始写程序时方便调试,其原理和驱动方式将在后续课程讲解,目前只需会用程序即可。

下面详细介绍模块化编程。首先对比传统编程方式和模块化编程的区别。传统编程把所有函数都放在 main.c 文件里,若使用的模块较多,一个文件内会有大量代码,不利于代码的组织和管理,还影响编程者思路。以之前写的动态数码管显示代码为例,其中的 Delay 函数和数码管显示部分都是模块,写好后几乎不需要更改,而真正需要设计者构思的是主函数里如何显示和控制的部分,这些驱动代码放在一起会显得碍眼,因此需要模块化编程,将各个模块独立出来放在不同的.c 文件里。

模块化编程的做法是把各个模块的代码放在不同的.c 文件中,在工程中添加新的.c 文件,将不需要更改的代码放进去。同时还有.h 文件,即头文件,头文件里提供外部可调用函数的声明,其他.c 文件想使用其中代码时,只需 #include "xxx.h" 即可。使用模块化编程可极大提高代码的可阅读性、可维护性和可移植性。例如,之前把各个驱动代码都放在 main.c 里,若要在其他程序中使用这些代码,复制时比较麻烦,且可能会夹杂其他代码,可移植性较差;而将其单独抽取出来放在.c 文件里,移植时只需复制粘贴.c 文件和对应的.h 文件,通过调用.h 文件里提供的模块接口,就能灵活运用该模块。

以 Delay 函数为例,若要将其模块化,需新建一个 Delay.c 文件并加到工程里,把 Delay 函数的主体和定义挪到 Delay.c 中,同时附带一个 Delay.h 文件。在 Delay.h 里,放的是 Delay.c 文件中需要被外部调用的接口,如 Delay 函数,只需将其函数定义的第一行复制粘贴过来并加上分号,前后再加上 #ifdef、#define、#endif 等预编译代码,以防止重复包含。在主函数里,不需要再定义 Delay 函数,只需 #include "Delay.h",就可直接调用里面的函数。

模块化编程有一些注意事项。.c 文件存放函数和变量的定义,.h 文件只放可被外部调用的函数和变量的声明,相当于一种协议。任何自定义的变量和函数,在使用前必须有定义或者声明,若在同一个.c 文件里使用的变量和函数未在使用前定义或声明,编译器可能会报错。若只有声明没有定义,虽然编译器不一定报错,但实际上存在问题,所以在整个工程里一定要有定义。使用到的自定义函数的.c 文件必须添加到工程参与编译,使用到的.h 文件必须放在编译器可寻找的地方

可自定义.h的位置如下,建议将.h 文件和 main.c 放在同一个文件夹里,方便编译器找到。

接着介绍 C 语言的预编译。预编译以井号开头,在真正的编译开始之前对代码做一些处理。例如 #include,它会把所包含文件的内容复制到当前位置,就像手动复制粘贴一样。而 #ifdef、#define、#endif 等预编译代码常用于模块化的头文件中,可防止重复定义。#define 可用于定义变量和进行名字替换,如 #define PI 3.14,预编译时会将代码里的 PI 全部替换为 3.14。

#ifdef 和 #endif 可对程序的某些部分是否编译进行选择,若定义了某个变量,相关代码才参与编译,否则不参与编译,这样可实现代码的可选化和精简,避免不需要编译的代码占据空间。

在定义头文件时,一般都会写上这些预编译代码,以防止重复包含。

下面通过实例将之前的代码模块化,本视频将对 Delay 函数和数码管显示进行模块化。像 LCD 比较容易模块化,而 LED 和按键模块化相对麻烦。

在模块化过程中,会遇到编码方式的问题。由于软件是外国的,对中文汉字兼容性不太好,若写注释打中文可能会出现乱码,删除时会出现删除一半字的情况。解决方法有多种,可在配置里选择编码方式,如 GB2312(简体中文),但选择后字体可能会自动改变,且修改字体可能会导致默认字体消失,不太建议修改。也可使用 UTF - 8 编码,它支持世界上所有语言,但使用 UTF - 8 编码后,若再用其他编码打开可能会出现乱码,不过英文不会出现乱码问题,建议使用 UTF - 8 编码。

正式开始模块化操作,新建一个工程,在新建的文件夹 "5 - 1 模块化编程" 里创建工程,选择 AT89C52 芯片,新建 main.c 文件,搭建基本程序框架,勾选 HEX 选项并编译,此时无错误。

最终要实现的效果是在 main.c 里 #include "Delay.h",就可直接使用 Delay 函数,而无需再定义函数体。

模块化 Delay 函数的步骤如下:

添加新的文件,命名为 Delay.c

将之前代码里的 Delay 函数体复制到 delay.c 中。

因为要 #include "Delay.h",所以还需新建一个 Delay.h 头文件,名字一般和对应的.c 文件相同。

将 Delay.h 添加到工程里

在Delay.h 里添加预编译代码,对 Delay 函数进行声明,声明是将函数定义的第一行复制过来并加上分号。

编译测试,无错误后,在main.c编写 LED 闪烁代码,编译并下载到单片机,可看到 LED 以 500 毫秒为周期闪烁,对比代码可发现 main.c 简洁了很多。

接着对数码管进行模块化,新建一个名为 "数码管" 的.c 文件和对应的.h 文件

将与数码管实现相关的代码复制到数码管.c 文件里。

注意在使用 P0 等自定义变量时,需在文件开头#include ,若使用 Delay 函数,同样直接 #include "Delay.h"。

编译时若出现函数未定义的警告,可忽略,因为还有剩余空间。

同时要在数码管.h 文件里对相应函数进行声明,声明时注意加上分号,否则编译可能出错

实现动态数码管显示只需三句代码,将其添加到 main.c 里。

编译无误后下载到单片机,可看到数码管显示 123,添加更多数字后再次编译下载,可看到显示 123456。对比之前的代码,现在的 main.c 代码量大幅减少,且都是实现逻辑的部分。若以后写代码需要用到数码管,只需复制数码管.c 和数码管.h 文件,#include 相关头文件并直接调用即可,但要注意数码管里用到了 Delay 函数,移植时也要移植 Delay 模块,以确保 Delay 函数有定义。

main.c

#include <REGX52.H>
#include "Delay.h"	//包含Delay头文件
#include "Nixie.h"	//包含数码管头文件

void main()
{
	while(1)
	{
		Nixie(1,1);	//在数码管的第1位置显示1
		Nixie(2,2);	//在数码管的第2位置显示2
		Nixie(3,3);	//在数码管的第3位置显示3
		Nixie(4,4);	//在数码管的第4位置显示4
		Nixie(5,5);	//在数码管的第5位置显示5
		Nixie(6,6);	//在数码管的第6位置显示6
	}
}

Delay.c

//延时子函数
void Delay(unsigned int xms)
{
	unsigned char i, j;
	while(xms--)
	{
		i = 2;
		j = 239;
		do
		{
			while (--j);
		} while (--i);
	}
}

Delay.h

#ifndef __DELAY_H__
#define __DELAY_H__

void Delay(unsigned int xms);

#endif

Nixie.c

#include <REGX52.H>
#include "Delay.h"	//包含Delay头文件

//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};

//数码管显示子函数
void Nixie(unsigned char Location,Number)
{
	switch(Location)		//位码输出
	{
		case 1:P2_4=1;P2_3=1;P2_2=1;break;
		case 2:P2_4=1;P2_3=1;P2_2=0;break;
		case 3:P2_4=1;P2_3=0;P2_2=1;break;
		case 4:P2_4=1;P2_3=0;P2_2=0;break;
		case 5:P2_4=0;P2_3=1;P2_2=1;break;
		case 6:P2_4=0;P2_3=1;P2_2=0;break;
		case 7:P2_4=0;P2_3=0;P2_2=1;break;
		case 8:P2_4=0;P2_3=0;P2_2=0;break;
	}
	P0=NixieTable[Number];	//段码输出
	Delay(1);				//显示一段时间
	P0=0x00;				//段码清0,消影
}

Nixie.h

#ifndef __NIXIE_H__
#define __NIXIE_H__

void Nixie(unsigned char Location,Number);

#endif

二、LCD1602调试工具

学好模块化编程之后,我们来看 LCD1602 调试工具。LCD 1602 使用 LCD 1602 屏幕作为调试窗口,主要用于调试。其实调试还有其他方式,如后续会学习的串口以及数码管等。最终选择 LCD 1602,是因为其调试较为方便。数码管可作为显示器件,能显示数字,但存在缺点,它需要不断扫描,一旦扫描不及时就会闪烁,且显示内容较少。串口可将数据发送到电脑上供观察,然而使用时需连接电脑并不断打开串口,同时串口也是一个需要学习的知识点。例如,做好的东西给别人演示时,连接电脑操作会显得麻烦。

使用 LCD 1602 时,将液晶屏插到开发板上,开发板上有短的排孔,把 1602 屏幕对齐插入排孔。

插入后,第一排显示全亮,第二排全灭。通过小螺丝刀调节定位器,左右拧可调节对比度,若显示太暗则调亮,若全是黑点则调暗。

需注意,使用 LCD 1602 后,数码管会显示乱码。从原理图可知,LCD 1602 连接的口是 P0 口,还占用了 P25、P26、P27 三个口,共占用 8 个口。P0 口也是数码管的接口,P2 口的这三个引脚是三个 LED 的引脚,会存在引脚冲突。所以连接液晶屏使用后,左边三个 LED 不能再用,会乱闪,数码管也因片选口被占用而不能使用。不过,LCD 1602 仅与数码管和这 3 个 LED 冲突,与其他器件引脚不冲突,因此使用它调试较为方便。

本教程提供 LCD1602 驱动代码,该代码是模块化代码,需要的可自行下载。代码中有两个文件,LCD1602.c 、LCD1602.h,使用者只需了解所提供函数的作用和使用方法,就能轻松使用 LCD1602。

其中包含LCD_Init();初始化函数,在使用 LCD1602之前必须调用一次进行初始化,一般上电后初始化一次即可。还有显示相关函数,如显示一个字符、显示字符串、显示数字、显示有符号数字、显示无符号数字、显示十六进制数字、显示二进制数字等,且各函数有相应参数。

接下来在 Keil 中新建一个工程,新建一个名为 "5 - 2" 的文件夹,用于 LCD1602 调试工具,选择 project ,at86c52,添加一个 main.c,勾上 hex。

体验 LCD 1602 模块化时,即便未学习 LCD1602 也能使用。先拿到两个文件(一个.c 和一个.h),使用 Ctrl + c 复制到工程目录,再通过 Ctrl + v 粘贴

然后右键添加已存在的文件,将这两个文件添加进来。

此时工程中就有写好的代码,每个函数的 API(接口)及参数注释都有,使用时要注意查看注释和参数范围,避免超出范围导致显示乱码。有些内部运转函数不需要调用,对外可见可调用的函数放在 LCD1602.h 中。

例如,LCD1602 初始化函数LCD_Init();需先调用,否则无法显示内容。显示字符函数 LCD_ShowChar有三个参数,分别为行、列和要显示的字符(字符需用单引号括起来)

如在一行一列显示字符 'a',仅需在main.c里相关两行代码。

编译时可能会出现警告,提示未调用某些函数

若觉得碍眼,可点击魔术棒,在 "BL51 Misc" 中找到 "disable warming numbers",输入警告编号(如 L16),点击 OK 可忽略警告。

下载代码后,可看到 LCD 第一个位置显示字符 'a',同时数码管会乱闪,左边三个灯的情况属正常现象。

显示字符串函数LCD_ShowString,可右键查看其定义,在 LCD 1602 指定位置开始显示所给字符串,参数为起始位置行、起始位置列和要显示的字符串(字符串用双引号括起来)。

如在一行三列显示 "hello",编译后可看到在指定位置显示该字符串,若超出右边边界则显示内容截止,参数超出范围可能显示乱码。

显示数字函数LCD_ShowNum 有四个参数,分别是行、列、要显示的数字和指定显示数字的长度。若指定长度小于数字实际位数,高位数字可能不显示;若指定长度大于实际位数,高位补 0。

例如在一行九列显示数字 123,指定长度为 3 位,编译下载后可看到在指定位置显示该数字。

显示有符号数字函数LCD_ShowSignedNum可显示负数

如在一行十三列显示负 66,指定长度为 2 位(不包括符号),编译后可看到显示结果。

显示十六进制数函数LCD_ShowHexNum 在调试某些东西时较为方便

如在 2 行一列显示 0 x A8,指定两位十六进制数,编译后可看到在指定位置显示相应十六进制数。

显示二进制数函数LCD_ShowBinNum

如在 2 行 4 列显示二进制数,需用十六进制表示(如 0 x AA代表 1010 1010),可用于数据验证。

例如验证 1 + 1 的结果,定义整型变量 result,让 result 等于 1 + 1,使用 LCD_ShowNum函数在一行一列显示 result,指定长度为三位,下载后可看到显示结果为 002,证明 1 + 1 = 2,可验证程序正确性。

若还想使用 Delay 模块,可将之前的 Delay 模块化代码 Ctrl + c 复制过来,Ctrl + v 粘贴到当前工程

添加已存在文件将 Delay 添加进来

然后#include "Delay.h",即可使用 Delay。

例如验证结果每秒加一,先将结果初始化为 0,使用 Delay 函数设置延时,如 1000毫秒,同时在每次延时后让结果加一,再通过LCD_ShowNum函数将结果显示出来,观察是否每秒加一,以此验证程序功能。

有了 LCD1602 这个调试工具,可方便地显示各种数据、字符串等内容。

main.c

#include <REGX52.H>
#include "LCD1602.h"	//包含LCD1602头文件
#include "Delay.h"		//包含Delay头文件

int Result=0;

void main()
{
	LCD_Init();
	while(1)
	{
		Result++;					//Result自增
		Delay(1000);				//延时1秒
		LCD_ShowNum(1,1,Result,3);	//在LCD的1行1列显示Result,长度为3位
	}
}

LCD1602.c

#include <REGX52.H>

//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0

//函数定义:
/**
  * @brief  LCD1602延时函数,12MHz调用可延时1ms
  * @param  无
  * @retval 无
  */
void LCD_Delay()
{
	unsigned char i, j;

	i = 2;
	j = 239;
	do
	{
		while (--j);
	} while (--i);
}

/**
  * @brief  LCD1602写命令
  * @param  Command 要写入的命令
  * @retval 无
  */
void LCD_WriteCommand(unsigned char Command)
{
	LCD_RS=0;
	LCD_RW=0;
	LCD_DataPort=Command;
	LCD_EN=1;
	LCD_Delay();
	LCD_EN=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602写数据
  * @param  Data 要写入的数据
  * @retval 无
  */
void LCD_WriteData(unsigned char Data)
{
	LCD_RS=1;
	LCD_RW=0;
	LCD_DataPort=Data;
	LCD_EN=1;
	LCD_Delay();
	LCD_EN=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602设置光标位置
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @retval 无
  */
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
	if(Line==1)
	{
		LCD_WriteCommand(0x80|(Column-1));
	}
	else if(Line==2)
	{
		LCD_WriteCommand(0x80|(Column-1+0x40));
	}
}

/**
  * @brief  LCD1602初始化函数
  * @param  无
  * @retval 无
  */
void LCD_Init()
{
	LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
	LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
	LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
	LCD_WriteCommand(0x01);//光标复位,清屏
}

/**
  * @brief  在LCD1602指定位置上显示一个字符
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的字符
  * @retval 无
  */
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
	LCD_SetCursor(Line,Column);
	LCD_WriteData(Char);
}

/**
  * @brief  在LCD1602指定位置开始显示所给字符串
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串
  * @retval 无
  */
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=0;String[i]!='\0';i++)
	{
		LCD_WriteData(String[i]);
	}
}

/**
  * @brief  返回值=X的Y次方
  */
int LCD_Pow(int X,int Y)
{
	unsigned char i;
	int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * @brief  在LCD1602指定位置开始显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~65535
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-32768~32767
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
	unsigned char i;
	unsigned int Number1;
	LCD_SetCursor(Line,Column);
	if(Number>=0)
	{
		LCD_WriteData('+');
		Number1=Number;
	}
	else
	{
		LCD_WriteData('-');
		Number1=-Number;
	}
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFF
  * @param  Length 要显示数字的长度,范围:1~4
  * @retval 无
  */
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i,SingleNumber;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		SingleNumber=Number/LCD_Pow(16,i-1)%16;
		if(SingleNumber<10)
		{
			LCD_WriteData(SingleNumber+'0');
		}
		else
		{
			LCD_WriteData(SingleNumber-10+'A');
		}
	}
}

/**
  * @brief  在LCD1602指定位置开始以二进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
	}
}

LCD1602.h

#ifndef __LCD1602_H__
#define __LCD1602_H__

//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);

#endif

Delay.c

void Delay(unsigned int xms)
{
	unsigned char i, j;
	while(xms--)
	{
		i = 2;
		j = 239;
		do
		{
			while (--j);
		} while (--i);
	}
}

Delay.h

#ifndef __DELAY_H__
#define __DELAY_H__

void Delay(unsigned int xms);

#endif
相关推荐
Moonnnn.38 分钟前
51单片机——程序执行过程(手工汇编)
汇编·笔记·嵌入式硬件·学习·51单片机
派阿喵搞电子1 小时前
STM32的APB1和APB2的区别
stm32·单片机·嵌入式硬件
电气_空空5 小时前
基于单片机及传感器的机器人设计与实现
单片机·嵌入式硬件·机器人·毕业设计·毕设
木燚垚6 小时前
基于STM32物联网水质监测系统的设计与实现/基于STM32的水产养殖云监控系统设计
stm32·单片机·嵌入式硬件·物联网·智能家居
QQ12971579408 小时前
51单片机 矩阵
单片机·嵌入式硬件·深度学习·算法·硬件工程·集成学习
Moonnnn.8 小时前
51单片机——汇编工程建立、仿真、调试全过程
汇编·笔记·嵌入式硬件·学习·51单片机
森焱森9 小时前
AArch64架构及其编译器
linux·c语言·单片机·架构
想要成为糕手。10 小时前
stm32-RTC时实时钟
stm32·嵌入式硬件·实时音视频
Czzzzlq11 小时前
STM32基础教程——对射式红外传感器计数实验
c语言·stm32·单片机·嵌入式硬件·mcu