江协科技/江科大-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
相关推荐
猫猫的小茶馆5 小时前
【ARM】ARM的介绍
c语言·开发语言·arm开发·stm32·单片机·嵌入式硬件·物联网
猫猫的小茶馆5 小时前
【PCB工艺】数模电及射频电路基础
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·pcb工艺
点灯小铭5 小时前
基于单片机的智能药物盒设计与实现
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
梓德原6 小时前
【基础】详细分析带隙型稳压电路的工作原理
单片机·嵌入式硬件·物联网
国科安芯7 小时前
航天医疗领域AS32S601芯片的性能分析与适配性探讨
大数据·网络·人工智能·单片机·嵌入式硬件·fpga开发·性能优化
小李做物联网7 小时前
【物联网毕业设计】60.1基于单片机物联网嵌入式项目程序开发之图像厨房监测系统
stm32·单片机·嵌入式硬件·物联网
贝塔实验室8 小时前
新手如何使用Altium Designer创建第一张原理图(三)
arm开发·单片机·嵌入式硬件·fpga开发·射频工程·基带工程·嵌入式实时数据库
@good_good_study8 小时前
STM32 ADC多通道采样实验
stm32·单片机·嵌入式硬件
Darken039 小时前
什么是“位带”?;在STM32单片机中有什么作用?
stm32·单片机·嵌入式硬件
清风66666610 小时前
基于单片机的智能豆浆机设计(加热打浆熬煮自动控制与防干溢保护)
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业