在查阅了很多关于 C51 单片机的程序后,个人感觉目前网上有关 C51 单片机程序的质量参差不齐,很多程序的代码风格及其糟糕,可读性也很差1。除了新手如此,很多写了多年程序的程序员老手也如此,也包括笔者还处于新手期的时候,在在乎程序是否可以正常运行,而忽略了程序的可读性、可维护性、可复用性以及可扩展性。
由于工作时,笔者所在的公司在编码风格和规范上有及其严格的规定,所以决定总结一下我对我在编写 C51 单片机程序时的编码规范。本文以猿学社上官一号开发板(核心为 STC89C52RC)为基础而编写,编码风格为 K&R(笔者之前干的是 Linux 内核驱动开发,工作用的就是这个风格),变量和函数使用下划线命名法。
本文前置要求:
- 已安装了 C51 的开发环境 Keil uVision5,未安装的建议参考博客《Keil MDK 与 Keil C51 共存的方法》,该文同时安装了 STM32 等主流 32 位单片机的开发环境,并解决了两个软件的共存问题。
- 下载 C51 单片机的烧录工具,软件下载地址:工具软件 STC-ISP
一、C51 单片机模板创建
模板的好处在于新建项目时,可以省去选型、添加必要文件等步骤,只要是同一个芯片的项目,复制模板后就可以直接开始编码工作。
模板已同步到 GitHub 和 Gitee,欢迎指正和查阅。
1. 新建工程及选型
运行 Keil uVision5,在最上面的菜单找到 Project
并点击,弹出下拉菜单后,选择第一个 New μVision Project
。
随后的弹窗中,为存放项目的文件夹,我个人的习惯是将所有的代码都放在一个名为 Code
的文件夹中,在里面再具体分类是什么类型的代码,如果是 C51 的代码会专门新建一个文件夹来存放,这里为了演示,直接在 D 盘新建了 C51 的文件夹。然后在文件夹中再新建一个文件夹,名为 template
,也就是模板的意思,双击进入文件夹,在下方的文件名再次输入 template
,点击 保存
。
接着是选型弹窗,由于我的 Keil 包含了 MDK 版本,所以在选型时要先切换到 Legacy Device Database
(意思是旧设备数据库),然后按下图操作即可。这里解释一下为什么选择 89C52,而不是 89C51,首先这两者的架构是一样的,都是 Intel 8051 架构,而上官一号采用的芯片是 STC89C52RC,故选 89C52;其次,C52 比 C51 多个更多的外设,例如,C51 拥有 4KB 的 ROM 作为程序存储空间,而 C52 是更大的 8KB,C51 只有两个 16 位定时器/计数器(T0 和 T1),而 C52 多一个额外的 16 位定时器/计数器(T2)等等。当然了,选择 89C51 也可能正常编译程序,并烧录进单片机运行,只是通常会选择与芯片相同的型号最佳。
不过这里也有人注意到了,现在的 C51 单片机基本都是 STC 生产的(深圳宏晶科技),为什么可以用 AT89C52 来代替 STC89C52 呢?其实这个无所谓选哪个,只要是 89C52 都可以,反正架构是一样的就行。
选好芯片型号后,在随后的弹窗中选择 是
,不然程序不能正常编译,这是 C51 的启动文件。
2. 创建主程序文件
在左边的项目侧边栏中,逐级点开 Target 1
-> Source Group 1
,然后鼠标右键点击 Source Group 1
,在弹出的菜单中选择 Add New Item to Group 'Source Group 1'
。
在随后的弹窗中,按下图所示操作,目的是创建 main.c
作为主程序文件。
创建成功后,输入下面的代码作为基础模板。
/********************************************************************
* File:
* Description:
* Version:
* Date:
* Author:
* ---------- Revision History ----------
* <version> <date> <author> <desc>
*
********************************************************************/
#include "main.h"
/* User Code Includes */
/* User Code Define */
/* User Code Global Variables */
/* User Code Function prototypes */
/* ------------------------------ Division Line ------------------------------ */
/**
* @brief Initialize all configured.
*/
void setup(void)
{
/* put your setup code here, to run once: */
}
/**
* @brief The application entry point.
*/
void main(void)
{
/* Private variables */
/* Initialize */
setup();
/* Infinite loop */
while (TRUE){
/* put your main code here, to run repeatedly: */
}
}
简单说明以下这个代码框架的含义,熟悉 Arduino 的同学应该一眼就看出我是抄了 Arduino IDE 的默认程序框架,setup()
是用于做初始化的函数,这个函数只运行一次,后面主要用于循环执行的程序则放在 mian()
函数中的 while(TRUE)
里。其他的注释基本是参考 CubeMX 生成的 Keil 代码,具体解释如下:
/* User Code Includes */
:在这个注释下可以添加其他头文件。/* User Code Define */
:在这个注释下添加程序需要的宏定义。/* User Code Global Variables */
:在这个注释下添加全局变量。/* User Code Function prototypes */
:在这个注释下添加函数原型。/* Private variables */
:main()
函数中还有/* Private variables */
,由于 C51 采用的是 C90 的编码标准,所有局部变量都只能写在函数最前端,故设此注释,可在此注释下添加main()
函数的局部变量。
最前面的一大块注释是文件的描述,这个后面再做具体的解释,当前效果如下图:
3. 创建主程序的头文件
相信大家也都注意到了,在主文件中的 main.h
头文件目前是不存在的,以及 while
循环的条件是 TRUE
,这个也是目前不存在的变量或宏定义,所以接下来就要创建对应的头文件。
与创建 .c 文件差不多,创建头文件只需要选择 Header File(.h)
即可。
然后在 main.h
文件中输入以下代码:
/********************************************************************
* File: bit.h
* Description: 89C51/89C52 main header files.
* Version: 1.0
* Date: 2024-04-25
* Author: zhengxinyu13@gmail.com
* ---------- Revision History ----------
* <version> <date> <author> <desc>
*
********************************************************************/
#ifndef __MAIN_H__
#define __MAIN_H__
#include "STC89C5xRC-rdp.h"
#define HIGH 1
#define LOW 0
#define TRUE 1
#define FALSE 0
#define TURN_ON 1
#define TURN_OFF 0
#define SET(pin) (pin) = 1
#define RESET(pin) (pin) = 0
typedef unsigned char uint8_t;
typedef signed char int8_t;
typedef unsigned int uint16_t;
typedef signed int int16_t;
typedef unsigned long uint32_t;
typedef signed long int32_t;
#include "delay.h"
#endif // __MAIN_H__
整个头文件用 #ifndef #define #endif
的结构,可以防止重复定义。__MAIN_H__
这个宏的命名方式也是行业约定俗成的命名方法,两个下划线开头,然后就是头文件名字的大写,中间的点用下划线代替,再两个下划线结尾。#endif
最后都会习惯用双斜杠注释最开始定义的宏,表示这个 #endif
是从哪个 #ifndef
开始的。
中间是一些常用的宏定义,typedef
关键字后面重定义了一些数据类型,这些类型别名使得代码更加可读,并且有助于提升代码的可移植性。这些数据类型取自 <stdint.h>
这个头文件,但是因为 C51 单片机使用的还是 C90 的标准,该头文件为 C99 标准时引入的头文件,因此没办法在这插入,所以才使用了typedef
关键字进行重定义。
regx51.h
头文件是 Keil 安装时就已经引入,不需要用户自定义,但是这个头文件并不完整,例如像特殊寄存器 AUXR、WDT_CONTR 等都未定义,也没有定义 P4 引脚,所以 STC89C5xRC-rdp.h
头文件对此进行了补充,该文件非笔者编写,而是由烧录软件 STC-ISP 自动生成。
具体代码如下:
/*--------------------------------------------------------------------------
STC89C5xRC-rdp.h
Supplemental header files for Low Voltage Flash Atmel AT89C52 and AT89LV52.
--------------------------------------------------------------------------*/
#ifndef __STC89C5xRC_RDP_H__
#define __STC89C5xRC_RDP_H__
/
//包含本头文件后,不用另外再包含"REG51.H"
sfr P0 = 0x80;
sbit P00 = P0^0;
sbit P01 = P0^1;
sbit P02 = P0^2;
sbit P03 = P0^3;
sbit P04 = P0^4;
sbit P05 = P0^5;
sbit P06 = P0^6;
sbit P07 = P0^7;
sfr SP = 0x81;
sfr DPL = 0x82;
sfr DPH = 0x83;
sfr PCON = 0x87;
sfr TCON = 0x88;
sbit TF1 = TCON^7;
sbit TR1 = TCON^6;
sbit TF0 = TCON^5;
sbit TR0 = TCON^4;
sbit IE1 = TCON^3;
sbit IT1 = TCON^2;
sbit IE0 = TCON^1;
sbit IT0 = TCON^0;
sfr TMOD = 0x89;
sfr TL0 = 0x8A;
sfr TL1 = 0x8B;
sfr TH0 = 0x8C;
sfr TH1 = 0x8D;
sfr AUXR = 0x8E;
sfr P1 = 0x90;
sbit P10 = P1^0;
sbit P11 = P1^1;
sbit P12 = P1^2;
sbit P13 = P1^3;
sbit P14 = P1^4;
sbit P15 = P1^5;
sbit P16 = P1^6;
sbit P17 = P1^7;
sbit T2EX = P1^1;
sbit T2 = P1^0;
sfr SCON = 0x98;
sbit SM0 = SCON^7;
sbit SM1 = SCON^6;
sbit SM2 = SCON^5;
sbit REN = SCON^4;
sbit TB8 = SCON^3;
sbit RB8 = SCON^2;
sbit TI = SCON^1;
sbit RI = SCON^0;
sfr SBUF = 0x99;
sfr P2 = 0xA0;
sbit P20 = P2^0;
sbit P21 = P2^1;
sbit P22 = P2^2;
sbit P23 = P2^3;
sbit P24 = P2^4;
sbit P25 = P2^5;
sbit P26 = P2^6;
sbit P27 = P2^7;
sfr AUXR1 = 0xA2;
sfr IE = 0xA8;
sbit EA = IE^7;
sbit EC = IE^6;
sbit ET2 = IE^5;
sbit ES = IE^4;
sbit ET1 = IE^3;
sbit EX1 = IE^2;
sbit ET0 = IE^1;
sbit EX0 = IE^0;
sfr SADDR = 0xA9;
sfr P3 = 0xB0;
sbit P30 = P3^0;
sbit P31 = P3^1;
sbit P32 = P3^2;
sbit P33 = P3^3;
sbit P34 = P3^4;
sbit P35 = P3^5;
sbit P36 = P3^6;
sbit P37 = P3^7;
sbit RD = P3^7;
sbit WR = P3^6;
sbit T1 = P3^5;
sbit T0 = P3^4;
sbit INT1 = P3^3;
sbit INT0 = P3^2;
sbit TXD = P3^1;
sbit RXD = P3^0;
sfr IPH = 0xB7;
sfr IP = 0xB8;
sbit PT2 = IP^5;
sbit PS = IP^4;
sbit PT1 = IP^3;
sbit PX1 = IP^2;
sbit PT0 = IP^1;
sbit PX0 = IP^0;
sfr SADEN = 0xB9;
sfr XICON = 0xC0;
sbit PX3 = XICON^7;
sbit EX3 = XICON^6;
sbit IE3 = XICON^5;
sbit IT3 = XICON^4;
sbit PX2 = XICON^3;
sbit EX2 = XICON^2;
sbit IE2 = XICON^1;
sbit IT2 = XICON^0;
sfr T2CON = 0xC8;
sbit TF2 = T2CON^7;
sbit EXF2 = T2CON^6;
sbit RCLK = T2CON^5;
sbit TCLK = T2CON^4;
sbit EXEN2 = T2CON^3;
sbit TR2 = T2CON^2;
sbit C_T2 = T2CON^1;
sbit CP_RL2 = T2CON^0;
sfr T2MOD = 0xC9;
sfr RCAP2L = 0xCA;
sfr RCAP2H = 0xCB;
sfr TL2 = 0xCC;
sfr TH2 = 0xCD;
sfr PSW = 0xD0;
sbit CY = PSW^7;
sbit AC = PSW^6;
sbit F0 = PSW^5;
sbit RS1 = PSW^4;
sbit RS0 = PSW^3;
sbit OV = PSW^2;
sbit F1 = PSW^1;
sbit P = PSW^0;
sfr ACC = 0xE0;
sfr WDT_CONTR = 0xE1;
sfr ISP_DATA = 0xE2;
sfr ISP_ADDRH = 0xE3;
sfr ISP_ADDRL = 0xE4;
sfr ISP_CMD = 0xE5;
sfr ISP_TRIG = 0xE6;
sfr ISP_CONTR = 0xE7;
sfr P4 = 0xE8;
sbit P40 = P4^0;
sbit P41 = P4^1;
sbit P42 = P4^2;
sbit P43 = P4^3;
sbit P44 = P4^4;
sbit P45 = P4^5;
sbit P46 = P4^6;
sbit P47 = P4^7;
sfr B = 0xF0;
/
#endif
代码也提示了,不需要再添加regx51.h
头文件了。
最后还有一个 delay.h
头文件,需要同时在工程中新建一个 delay.h
和一个 delay.c
,内容如下:
/********************************************************************
* File: delay.h
* Description: Time delay function based on 11.0592MHz crystal
* oscillator.
* crystal oscillator.
* Version: 1.0
* Date: 2023-07-17
* Author: zhengxinyu13@gmail.com
* ---------- Revision History ----------
* <version> <date> <author> <desc>
*
********************************************************************/
#ifndef __DELAY_H__
#define __DELAY_H__
#include "main.h"
#include "intrins.h"
void delay_ms(uint16_t xms);
void delay_1s(void);
#endif // __DELAY_H__
/********************************************************************
* File: delay.h
* Description: Delay function library based on 11.0592MHz
* crystal oscillator.
* Version: 1.0
* Date: 2023-07-17
* Author: zhengxinyu13@gmail.com
* ---------- Revision History ----------
* <version> <date> <author> <desc>
*
********************************************************************/
#include "delay.h"
/**
* @brief Delay function in milliseconds.
* @param Enter the delay time in milliseconds.
*/
void delay_ms(uint16_t xms)
{
uint16_t i,j;
for (i = 0; i < xms; i++)
for (j = 0; j < 112; j++);
}
/**
* @brief Delay by one second.
*/
void delay_1s(void)
{
uint8_t i, j, k;
_nop_();
i = 8;
j = 1;
k = 243;
do {
do {
while (--k);
} while (--j);
} while (--i);
}
这里特殊说明一下,这个延时函数的文件不是必要的,只是延时函数在 C51 的开发过程中太常见了,所以加上去方便后面的开发。
[!NOTE]
实际项目的单片机开发不会用在这种软件延时,太占用 CPU 了,通常是用定时中断计时,考虑到 C51 的资源太匮乏,在学习开发阶段,可以使用软件延时。
4. 编译配置
最后需要配置编译,否则编译后也不会生成 .hex
文件,在菜单栏中找打类似魔术棒一样的图标并点击。
这里建议把晶振频率设置成实际开发的频率,一般都是 11.0592MHz。
然后点击选项卡 Output
,把 Create HEX File
勾上,Name of Executable
是编译后的 .hex
文件的文件名,我习惯使用 build
。最后点击 OK
完成配置。
点击编译试一下,如果在下面的 Bulid Output
中出现 creating hex file from ".Objectsbuild"...
的字样,就表示编译通过,并生成了 .hex
文件。有两个警告是因为 delay.c
中写了两个延时函数没有调用导致,知道原因可以忽略。
5. 其他
考虑到很多英文水平不好的同学写英文注释比较难受,可以使用中文注释,建议修改成 UTF-8。先在菜单栏找到小扳手的图标并打开,在 Encoding
中选择 Encode in UTF-8 without signature
,不建议选择 GB2312
或者 BIG5
,因为有些字符无法显示。
如果有同学对我的 Keil 主题比较感兴趣的话,我可以单独出一篇博客细说一下。
二、C51 的编码规范
关于编码规范,我基本都有比较统一的标准,可以直接参考我的博客《关于我个人的编码规范(C/C++)》,这里只补充一点,这规范中,我提到要将 tab
键设置成 4 个空格,统一用 tab
键缩进,这里教大家怎么把 Keil 的 tab
设置成 4 个空格。
按图设置即可。
-
个人感觉郭天祥老师有一定责任,当年他培养出新世纪以来第一批嵌入式工程师,他的十天学会 C51 的课程以及那本新概念的书功不可没,但是其课程和书的代码风格都很乱,这也可能是后来很多单片机工程师代码风格很差的一个原因。 ↩︎