代码整理_2
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.h、P1与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、P2 | reg52.h |
uchar、uint | main.h自己的 typedef |
recv | 在 main.c中定义,在 main.h中用 extern声明 |
LED1~LED4、BEEP、JDQ1 | main.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硬件]
完整操作顺序如下:
- 在工程目录建立
Uart文件夹。 - 使用Keil或纯文本编辑器创建
uart.c和uart.h,确认不存在空格或隐藏的.txt后缀。 - 把
uart.c加入Keil的Source Group,使其参与编译。 - 在
Options for Target → C51 → Include Paths中加入./Uart。 - 把串口发送函数放入
uart.c,把相应声明放入uart.h。 uart.c和调用串口接口的main.c都包含uart.h。- 把串口寄存器配置封装为
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时展开三次;有保护后,只有第一次保留正文。
头文件保护有两个重要边界:
- 它防止的是同一翻译单元中的重复展开。
main.c和uart.c分别编译时,各自都会正常处理一次main.h。 - 它不能修复头文件中不应出现的普通全局变量定义。共享变量仍应采用
.c中唯一一次定义、.h中extern声明的方式。
保护宏必须唯一。配套代码使用 _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模块负责串口初始化和发送,公共头文件负责接口与共享声明。模块之间通过明确的声明连接,而不是靠复制代码或偶然的包含顺序工作。