在查阅了很多关于 C51 单片机的程序后,个人感觉目前网上有关 C51 单片机程序的质量参差不齐,很多程序的代码风格及其糟糕,可读性也很差1。除了新手如此,很多写了多年程序的程序员老手也如此,也包括笔者还处于新手期的时候,在在乎程序是否可以正常运行,而忽略了程序的可读性、可维护性、可复用性以及可扩展性。

由于工作时,笔者所在的公司在编码风格和规范上有及其严格的规定,所以决定总结一下我对我在编写 C51 单片机程序时的编码规范。本文以猿学社上官一号开发板(核心为 STC89C52RC)为基础而编写,编码风格为 K&R(笔者之前干的是 Linux 内核驱动开发,工作用的就是这个风格),变量和函数使用下划线命名法。

在这里插入图片描述

本文前置要求:

  1. 已安装了 C51 的开发环境 Keil uVision5,未安装的建议参考博客《Keil MDK 与 Keil C51 共存的方法》,该文同时安装了 STM32 等主流 32 位单片机的开发环境,并解决了两个软件的共存问题。
  2. 下载 C51 单片机的烧录工具,软件下载地址:工具软件 STC-ISP

一、C51 单片机模板创建

模板的好处在于新建项目时,可以省去选型、添加必要文件等步骤,只要是同一个芯片的项目,复制模板后就可以直接开始编码工作。

模板已同步到 GitHub 和 Gitee,欢迎指正和查阅。

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 个空格。

按图设置即可。

在这里插入图片描述


  1. 个人感觉郭天祥老师有一定责任,当年他培养出新世纪以来第一批嵌入式工程师,他的十天学会 C51 的课程以及那本新概念的书功不可没,但是其课程和书的代码风格都很乱,这也可能是后来很多单片机工程师代码风格很差的一个原因。 ↩︎