代码整理_2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

055.7.2 代码整理(2)

/******************** main.c ********************/
#include "reg52.h"
#include <intrins.h>
#include "delay.h"
#include "main.h"
#include "uart.h"

uchar recv;

void main()
{
    UART_Init();

    while (1)
    {
        UART_Send_Str("I am wfeng!\r\n");
        Delay_xms(1000);
    }
}

/*****************
串口中断处理函数,接收数据
*****************/
void UART_Routine(void) interrupt 4
{
    if (RI == 1) // 判断接收标志位是否为1
    {
        RI = 0;        // RI接收标志位清0
        recv = SBUF;   // 接收数据

        switch (recv)
        {
            case 0x01:
                LED1 = 0;
                break;
            case 0x02:
                LED2 = 0;
                break;
            case 0x03:
                LED3 = 0;
                break;
            case 0x04:
                LED4 = 0;
                break;
            case 0x05:
                P1 |= 0x0F;
                break;
            case 0x06:
                BEEP = 0;
                break;
            case 0x07:
                BEEP = 1;
                break;
            case 0x08:
                JDQ1 = 0;
                break;
            case 0x09:
                JDQ1 = 1;
                break;
            default:
                break;
        }
    }
}

/******************** main.h ********************/
#ifndef _MAIN_H_
#define _MAIN_H_

#include "reg52.h"

typedef unsigned char uchar;
typedef unsigned int uint;

extern uchar recv;

sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit BEEP = P1^6;
sbit JDQ1 = P2^0;

#endif

/******************** Delay/delay.h ********************/
#ifndef _DELAY_H_
#define _DELAY_H_

#include "main.h"

void Delay_xms(uint xms);

#endif

/******************** Delay/delay.c ********************/
#include "delay.h"
#include <intrins.h>

void Delay_xms(uint xms) //@11.0592MHz
{
    uchar data i, j;

    while (xms)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
        xms--;
    }
}

/******************** Uart/uart.h ********************/
#ifndef _UART_H_
#define _UART_H_

#include "main.h"

void UART_Send_Byte(uchar send_byte);
void UART_Send_Str(uchar *send_str);
void UART_Init(void);

#endif

/******************** Uart/uart.c ********************/
#include "uart.h"
#include "main.h"

// 串口发送一个字节
void UART_Send_Byte(uchar send_byte)
{
    SBUF = send_byte;
    while (!TI);
    TI = 0;
}

// 串口发送字符串
void UART_Send_Str(uchar *send_str)
{
    while (*send_str != '\0') // 检测字符串结束符
    {
        UART_Send_Byte(*send_str++); // 发送一字节后,指针移向下一字节
    }
}

void UART_Init(void)
{
    SCON = 0x50;   // 串口工作方式1,并允许接收
    PCON &= 0x7F;  // 波特率不加倍
    TMOD &= 0x0F;
    TMOD |= 0x20;  // 定时器1方式2,8位自动重装
    TH1 = 0xFD;
    TL1 = 0xFD;    // 11.0592MHz、12T、SMOD=0时为9600bit/s
    TR1 = 1;       // 启动定时器1
    ES = 1;        // 打开串口中断
    EA = 1;        // 打开总中断
    recv = 0;
}

一、代码整理

1.1 精简代码

延时函数已经放入 Delay/delay.c,公共类型、引脚名称和跨文件变量声明集中放入 main.h。这里所谓“精简”,不是删除功能,而是把不同职责交给不同模块,使 main.c只保留程序入口、主循环和本讲仍放在主模块中的串口接收中断。

整理后的 main.c主体非常直接:

void main()
{
    UART_Init();

    while (1)
    {
        UART_Send_Str("I am wfeng!\r\n");
        Delay_xms(1000);
    }
}

从主函数可以立即看出程序行为:先初始化UART,再每隔约1秒发送一次字符串。延时算法和串口寄存器配置都被隐藏在对应模块内部。

视频中把 sbit引脚定义移入 main.h后,编译器提示 P1未定义。原因不是LED引脚接线发生变化,而是C编译器处理 main.h时,必须先知道符号 P1代表什么。

reg52.hP1与LED别名的依赖关系如下:

reg52.h
   │
   ├── 声明P0、P1、P2、P3等51单片机特殊功能寄存器
   │
   ▼
main.h
   ├── sbit LED1 = P1^0;
   ├── sbit LED2 = P1^1;
   ├── sbit LED3 = P1^2;
   └── sbit LED4 = P1^3;

因此 main.h需要先包含器件寄存器头文件:

#include "reg52.h"

在Keil C51语法中:

sbit LED1 = P1^0;

表示把位变量 LED1映射到P1口的第0位。此处的 P1^0位于Keil的 sbit声明语法中,表示SFR的位地址,并不是普通C表达式中的按位异或计算。

P1特殊功能寄存器

位序号:   7    6    5    4    3    2    1    0
         ┌────┬────┬────┬────┬────┬────┬────┬────┐
P1:     │P1.7│P1.6│P1.5│P1.4│P1.3│P1.2│P1.1│P1.0│
         └────┴────┴────┴────┴────┴────┴────┴────┘
                                      │    │    │    │
                                     LED4 LED3 LED2 LED1

一个头文件应尽量做到“自包含”:它公开的声明用到了什么类型或寄存器,就应包含能够提供这些名称的头文件,不能依赖调用者碰巧先包含了某个文件。否则改变 #include顺序就可能出现难以理解的未定义错误。

main.h中使用的名称来源
P1、P2reg52.h
uchar、uintmain.h自己的 typedef
recvmain.c中定义,在 main.h中用 extern声明
LED1~LED4、BEEP、JDQ1main.h中的Keil sbit声明

1.2 将代码放到不同的文件

串口相关代码可封装为独立的Uart模块:

Uart/
├── uart.h   对外公开“可以调用什么”
└── uart.c   保存“具体怎样实现”

uart.h公开三个接口:

void UART_Send_Byte(uchar send_byte);
void UART_Send_Str(uchar *send_str);
void UART_Init(void);

uart.c保存三个函数的完整实现。其他文件只要包含 uart.h,就能按照统一的函数签名调用这些接口。

flowchart LR
    M[main.c] -->|包含并调用| H[uart.h 公共接口]
    C[uart.c] -->|包含并实现| H
    M -->|调用UART_Init| O1[uart.obj中的UART_Init]
    M -->|调用UART_Send_Str| O2[uart.obj中的UART_Send_Str]
    O2 --> O3[UART_Send_Byte]
    O3 --> S[SBUF与TXD硬件]

完整操作顺序如下:

  1. 在工程目录建立 Uart文件夹。
  2. 使用Keil或纯文本编辑器创建 uart.cuart.h,确认不存在空格或隐藏的 .txt后缀。
  3. uart.c加入Keil的Source Group,使其参与编译。
  4. Options for Target → C51 → Include Paths中加入 ./Uart
  5. 把串口发送函数放入 uart.c,把相应声明放入 uart.h
  6. uart.c和调用串口接口的 main.c都包含 uart.h
  7. 把串口寄存器配置封装为 UART_Init(),并在 main()进入循环前调用。

.c文件必须加入工程构建;.h不单独编译,只需能被 #include找到。把头文件显示在工程树中可以方便浏览,但不是链接函数实现的必要条件。

声明、定义和调用的写法

/* uart.h:函数声明,分号结尾 */
void UART_Init(void);

/* uart.c:函数定义,带函数体 */
void UART_Init(void)
{
    /* 串口初始化代码 */
}

/* main.c:函数调用,只写实参列表和分号 */
UART_Init();

声明与定义中的 void表示该函数不接收参数;调用时括号里没有实参,不能写成 UART_Init(void);。后者在函数体内会被当作声明形式,而不是正常的函数调用语句。

UART_Init()封装了什么

flowchart TD
    I[调用UART_Init] --> A[SCON=0x50<br/>方式1并允许接收]
    A --> B[PCON.SMOD=0<br/>波特率不加倍]
    B --> C[定时器1方式2<br/>TH1=TL1=0xFD]
    C --> D[TR1=1<br/>启动定时器1]
    D --> E[ES=1、EA=1<br/>允许串口中断]
    E --> F[UART可收发数据]

在11.0592 MHz、传统12T、SMOD=0的条件下,0xFD产生9600 bit/s。模块化只把这些语句移入函数,并未改变寄存器配置。

配套代码最后才执行 recv = 0;,但此前已经执行 EA = 1。更稳妥的初始化顺序是先给共享状态设置初值,最后再开放中断,避免在极短的时间窗口内发生接收中断后,刚收到的数据又被初始化语句覆盖:

recv = 0;
ES = 1;
EA = 1;

Keil中的 Go to Definition可以根据工程符号信息跳到函数定义。若无法跳转,应先确认:

  • 函数声明与定义的名称、大小写和参数完全一致;
  • 对应 .c已经加入工程;
  • 工程已经成功编译并建立浏览信息;
  • 头文件搜索路径正确。

1.3 外部声明

每个 .c独立编译。main.c中的全局变量 recv不会因为另一个文件也属于同一工程,就自动在 uart.c中变成已知名称。跨文件共享普通全局变量需要“一处定义,多处 extern声明”。

/* main.c:定义,分配唯一的存储空间 */
uchar recv;

/* main.h:外部声明,不再分配一份存储空间 */
extern uchar recv;

/* uart.c:包含声明后即可引用同一变量 */
#include "main.h"

void UART_Init(void)
{
    recv = 0;
}

编译和链接过程可以这样理解:

flowchart LR
    A[main.c<br/>uchar recv] --> A1[main.obj<br/>提供符号recv及存储]
    B[main.h<br/>extern uchar recv] --> C[uart.c使用recv]
    C --> C1[uart.obj<br/>产生对recv的外部引用]
    A1 --> L[链接器]
    C1 --> L
    L --> R[把uart.obj中的recv引用<br/>连接到main.obj的唯一存储]

定义与声明的区别

写法性质是否分配存储应放在哪里
uchar recv;定义只放在一个 .c
uchar recv = 0;带初值的定义只放在一个 .c
extern uchar recv;外部声明可放在公共 .h
static uchar recv;文件内部定义仅当前 .c可见,不能作为同名外部变量共享

extern不是“把变量变成全局变量”的魔法。它告诉编译器:该名称和类型已经声明,真正定义由链接器在其他目标文件中寻找。要成功连接,必须满足:

  • 某一个 .c中确实存在且只存在一个外部定义;
  • 声明与定义的类型完全一致;
  • 定义不能带 static内部链接;
  • 提供定义的 .c必须参与工程构建。

为什么 extern声明不应写初始化值

推荐写法是:

/* main.h */
extern uchar recv;

/* main.c */
uchar recv = 0;

不要在公共头文件中写:

extern uchar recv = 0;

在标准C的文件作用域中,带初始化器的 extern会成为定义,而不再是单纯的外部声明;某些C51编译器还可能直接拒绝这种形式。若头文件被多个 .c包含,就可能生成多份定义并导致编译或链接错误。因此,初始化值应写在唯一的 .c定义处。

类型必须先被声明

编译器从上到下处理声明。下面的顺序正确:

typedef unsigned char uchar;
extern uchar recv;

如果 extern uchar recv;出现在 uchar类型别名之前,编译器尚不知道 uchar是什么,会在类型附近报错。

与中断共享时的 volatile

recv由串口中断函数写入。如果主循环或其他普通函数也要持续读取它,建议声明成:

/* main.c */
volatile uchar recv;

/* main.h */
extern volatile uchar recv;

volatile告诉编译器,该变量可能被中断等当前执行流之外的因素改变,每次使用时都应按规则重新访问存储位置。声明和定义必须同时保留相同的 volatile限定。

volatile只解决编译器优化可见性,不自动保证复杂操作的原子性。若主程序与中断共同读写多字节变量或执行“读—改—写”操作,还要考虑临界区或短暂屏蔽相关中断。

1.4 任务信息资源导图组件库总结

头文件保护的工作过程

main.h为例:

#ifndef _MAIN_H_
#define _MAIN_H_

/* main.h的有效内容 */

#endif

#ifndef的意思是“如果这个宏尚未定义”。第一次包含时,保护宏还不存在,正文被保留,并立即执行 #define;同一翻译单元再次包含时,条件为假,正文被跳过。

main.c的一次预处理

第1次遇到main.h
  _MAIN_H_未定义 → 定义宏 → 展开正文

第2次遇到main.h
  _MAIN_H_已定义 → 跳过正文

第3次遇到main.h
  _MAIN_H_已定义 → 跳过正文

配套代码存在多条间接包含路径:

flowchart TD
    M[main.c] --> D[delay.h]
    M --> U[uart.h]
    M --> H[main.h]
    D --> H
    U --> H

没有头文件保护时,main.h可能在编译 main.c时展开三次;有保护后,只有第一次保留正文。

头文件保护有两个重要边界:

  1. 它防止的是同一翻译单元中的重复展开。main.cuart.c分别编译时,各自都会正常处理一次 main.h
  2. 它不能修复头文件中不应出现的普通全局变量定义。共享变量仍应采用 .c中唯一一次定义、.hextern声明的方式。

保护宏必须唯一。配套代码使用 _MAIN_H__DELAY_H__UART_H_;编写自己的头文件时,建议避免以下划线加大写字母开头,采用更不易冲突的名称:

#ifndef VIDEO_LEARN_UART_H
#define VIDEO_LEARN_UART_H

/* 声明 */

#endif

双引号与尖括号

#include "uart.h"
#include <intrins.h>

两者的核心区别是编译器采用的搜索路径和顺序,不是文件内容本身发生变化。通常约定:

  • "文件名"用于工程自己的头文件,优先从当前工程或源文件附近寻找,再查配置的包含目录。
  • <文件名>用于编译器、标准库或芯片工具链提供的头文件,主要从系统和配置的包含目录寻找。

具体搜索顺序由编译器实现决定。Keil C51中,双引号形式会先搜索当前目录和源文件目录,找不到后再按尖括号形式继续搜索;Include Paths决定额外的头文件目录。

reg52.h通常也是Keil提供的器件头文件,因此写成 <reg52.h>更能表达它来自工具链;配套工程使用 "reg52.h"仍可由Keil的搜索规则找到对应文件。intrins.h提供 _nop_()等Keil内在函数声明。

整理后的关键规则

规则正确做法
功能拆分一个模块使用对应的 .c/.h组织实现与接口
函数接口.h声明,.c定义,调用者包含 .h
源文件构建每个实现函数所在的 .c必须加入工程
头文件路径把头文件所在目录加入C51的Include Paths
公共变量一个 .c定义,公共 .h中写 extern声明
中断共享变量根据访问方式考虑 volatile与原子性
重复包含每个头文件使用唯一保护宏
依赖顺序头文件先获得它所使用的类型、寄存器和宏定义
排查错误从第一条有效错误开始,重新编译后再看后续问题

经过整理,main.c只负责组织流程,Delay模块负责延时,Uart模块负责串口初始化和发送,公共头文件负责接口与共享声明。模块之间通过明确的声明连接,而不是靠复制代码或偶然的包含顺序工作。