代码优化
035.3.3 代码优化
#include "reg52.h"
#include <intrins.h>
sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit KEY1 = P3^2;
sbit KEY2 = P3^3;
sbit SUN = P3^7;
typedef unsigned char uchar;
typedef unsigned int uint;
//void Delay20ms(void) // @11.0592MHz
//{
// unsigned char data i, j;
// i = 36;
// j = 217;
// do
// {
// while (--j);
// } while (--i);
//}
void Delay_xms(uint xms) // @11.0592MHz
{
uchar data i, j;
while (xms)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
while (1)
{
//Delay_xms(1000);
if (0 == KEY1)
{
Delay_xms(20);
while (0 == KEY1); // while(1);松手检查
Delay_xms(20);
LED1 = ~LED1;
}
if (0 == KEY2)
{
Delay_xms(20);
while (0 == KEY2); // while(1);松手检查
Delay_xms(20);
LED2 = ~LED2;
}
if (SUN == 0)
{
LED3 = 1;
}
else
{
LED3 = 0;
}
}
}
一、代码优化
本节由 3.2输入检测 工程复制出 3.3输入检测_代码优化 工程,在不改变按键和光敏检测功能的前提下,提高延时函数的复用性、条件判断的安全性以及数据类型的可读性。
代码仍对应以下硬件连接:
输入端 输出端
SW3 按下接地 ──► P3.2 / KEY1 ───────► P1.0 / LED1
SW4 按下接地 ──► P3.3 / KEY2 ───────► P1.1 / LED2
光敏模块 DO ───► P3.7 / SUN ───────► P1.2 / LED3
按键:按下为 0,松开为 1
LED :P1.x 输出 0 时点亮,输出 1 时熄灭
按键和 LED 都是低电平有效,但前者是因为开关闭合后把端口接到 GND,后者是因为 LED 从 +5V 经限流电阻接到单片机端口,端口输出 0 时形成吸电流回路。代码优化不能脱离这些电路事实,否则即使语法正确,也可能得到相反的灯光现象。
1.1 优化延时函数
固定的 Delay20ms() 只能延时约 20 ms。若还需要 30 ms、100 ms 或 1000 ms,就要创建多个高度重复的函数。更通用的办法是先生成约 1 ms 的基础延时代码,再用参数决定重复次数:
flowchart LR
A[调用 Delay_xms 20] --> B[xms 初值为 20]
B --> C{是否大于 0}
C -- 是 --> D[执行一次约 1 ms 的延时代码]
D --> E[xms 减 1]
E --> C
C -- 否 --> F[函数返回]
核心结构是:
void Delay_xms(unsigned int xms)
{
while (xms)
{
/* 约 1 ms 的基础延时代码 */
xms--;
}
}
当调用 Delay_xms(20); 时,形参 xms 得到初值 20。循环每执行一次,完成约 1 ms 忙等待并将 xms 减 1;减到 0 后,while(0) 为假,函数退出。Delay_xms(30)、Delay_xms(1000) 的原理完全相同。
| 调用方式 | 基础延时执行次数 | 名义延时 |
|---|---|---|
Delay_xms(0) | 0 | 0 ms |
Delay_xms(20) | 20 | 约 20 ms |
Delay_xms(30) | 30 | 约 30 ms |
Delay_xms(1000) | 1000 | 约 1 s |
这里要注意五个细节:
- 参数是值传递:函数内部递减的是局部形参
xms,不会改变调用处写下的常量,也不会影响其他变量。 - 函数调用后要写分号:正确形式是
Delay_xms(20);。 _nop_()需要声明:该内建函数由 Keil C51 的<intrins.h>声明,因此源码必须包含该头文件。- 旧函数不再参与编译:
Delay20ms()已整体注释。若同时保留定义却不调用,Keil 链接器可能给出UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS警告,表示该函数对应的代码段没有调用者,通常会被忽略。警告不一定阻止生成 HEX,但仍应理解原因并清理无用代码。 - 这是忙等待:延时期间 CPU 只在执行空循环,不能处理其他普通任务。后续需要并行处理显示、串口和多个输入时,应使用定时器或基于系统节拍的非阻塞逻辑。
1 ms 基础代码由 11.0592 MHz、STC89 对应的指令时序生成。把原本独立的 1 ms 函数体嵌入外层循环后,还会增加 while (xms)、xms-- 和循环跳转等开销,所以 x ms 是近似值而不是高精度计时。晶振频率、1T/6T/12T 模式、编译器版本和优化级别变化后,都应重新测量或生成延时。
完成构建后,应从 3.3输入检测_代码优化/Objects 目录选择当前生成的 HEX,并结合路径与时间戳核对,避免烧录到其他工程的产物。KEY1 和 KEY2 分别调用新延时函数后,仍应保持一次完整按压只翻转一次对应 LED。
1.2 优化判断语句
判断相等使用 ==,赋值使用 =:
if (KEY1 == 0) /* 判断 KEY1 是否等于 0 */
if (KEY1 = 0) /* 把 0 写入 KEY1,再判断赋值表达式的结果 */
第二行在 C 语言中是合法语法。它会把 KEY1 对应的 P3.2 端口位写成 0,赋值表达式本身的结果也是 0,因此条件为假。对于 sbit,这不只是普通变量写错,还可能直接改变硬件引脚输出状态。
把常量放在左侧可以让漏写等号更容易在编译期暴露:
if (0 == KEY1) /* 正常判断 */
if (0 = KEY1) /* 非法:不能给常量 0 赋值,编译器报错 */
这种写法有时称为“常量在左”或 Yoda condition。KEY1 == 0 本身并不错误,现代编译器通常也可以针对条件中的可疑赋值发出警告;无论采用哪种风格,都要区分 == 和 =,并认真查看 Warning。配套源码把 KEY1、KEY2 写成 0 == KEYx,逻辑与 KEYx == 0 完全相同。
条件判断依旧受电路极性约束:
SW3/SW4 松开:P3.2/P3.3 由内部弱上拉保持为 1
SW3/SW4 按下:端口经按键接 GND,读到 0
因此 if (0 == KEY1) 表示“KEY1 被按下”。进入分支后,程序延时消抖、等待松手、再次延时,然后执行一次 LED1 = ~LED1。
光敏模块没有机械弹片,不会产生按键式机械抖动,所以源码直接判断 SUN,不需要等待“松手”。不过,环境光位于电位器阈值附近时,LM393 的 DO 可能因微小光照变化或噪声反复翻转;这属于阈值抖动,可通过调节电位器、连续多次采样、时间滤波或迟滞处理改善。
光敏输入到 LED3 的电平链路为:
光强较强 ─► 光敏电阻阻值下降 ─► LM393 的 DO=0 ─► SUN=0
─► 程序写 LED3=1 ─► 低电平有效的 LED3 熄灭
光线较弱 ─► 光敏电阻阻值上升 ─► LM393 的 DO=1 ─► SUN=1
─► 程序写 LED3=0 ─► LED3 点亮
1.3 优化int类型
C 语言只规定各整数类型的最小能力和相对大小,具体位宽由编译器和目标平台决定。对本课程使用的 Keil C51 编译环境,常用整数类型如下:
| 类型 | 位宽 | 字节数 | 取值范围 |
|---|---|---|---|
signed char | 8 位 | 1 Byte | -128~127 |
unsigned char | 8 位 | 1 Byte | 0~255 |
signed int | 16 位 | 2 Bytes | -32768~32767 |
unsigned int | 16 位 | 2 Bytes | 0~65535 |
signed long int | 32 位 | 4 Bytes | -2147483648~2147483647 |
unsigned long int | 32 位 | 4 Bytes | 0~4294967295 |
对于 N 位无符号整数,全部 N 位都表示数值,范围为:
$ 0\sim 2^N-1 $
所以 8 位无符号数最大值为 $2^8-1=255$,16 位无符号数最大值为 $2^{16}-1=65535$。一共仍分别有 $2^8=256$、$2^{16}=65536$ 个不同状态,因为计数从 0 开始。
在常见的二进制补码表示中,N 位有符号整数的范围为:
$ -2^{N-1}\sim 2^{N-1}-1 $
因此 16 位 int 为 -32768~32767。正负范围看起来不完全对称,是因为 0 也占用一个编码,负数一侧多出一个值。
延时参数选择 unsigned int 的原因是:
- 毫秒数不需要负值,使用无符号类型更符合含义;
unsigned char最大只能保存 255,无法直接表示Delay_xms(1000)的 1000;- C51 的
unsigned int可保存 0~65535,足以覆盖本实验的常用毫秒数。
函数内部的 i=2、j=199 都没有超过 255,因此使用 unsigned char 可以节省存储空间。在 uchar data i, j; 中,data 还是 Keil C51 的存储区限定符,表示把变量放在内部可直接寻址 DATA 区;它不是变量名,也不是标准 C 数据类型的一部分。
char 不只能保存可见字符。计算机中的字符同样以数值编码保存,例如在常见 ASCII 编码下,字符 'A' 的数值是 65;同一个 8 位类型也可以保存普通小整数或原始字节。char 与 int 的主要区别包括位宽、范围、运算规则和在目标平台上的访问效率,而不是“一个只能存字符、另一个只能存数字”。
在常见 ARM 工具链中,int 通常为 32 位,但这是具体 ABI 和编译器的约定,不能只凭 C 关键字假定所有平台都相同。跨平台代码若必须精确表达位宽,应优先使用 <stdint.h> 中的 uint8_t、uint16_t、uint32_t 等定宽类型,并通过目标编译器再次确认。
还要注意溢出:Delay_xms 的参数是 16 位,传入超过 65535 的整数时,转换后的值可能不再是期望的毫秒数;即使传入 60000,阻塞约一分钟也会让主程序长时间无法响应按键和光敏输入。长时间计时应改用硬件定时器或系统时基。
1.4 优化类型别名
typedef 可以为已有类型定义更简洁的别名:
typedef unsigned char uchar;
typedef unsigned int uint;
语法顺序是:
typedef + 已有类型 + 新别名 + 分号
typedef unsigned int uint;
└────原类型───┘ └新名称┘
定义后,下面两组声明完全等价:
unsigned int xms;
uint xms;
unsigned char data i, j;
uchar data i, j;
typedef 不会创建一种具有新位宽的新整数,也不会改变取值范围、内存布局或运行效率;它只是给现有类型增加一个名称。uint 在本工程中仍然就是 Keil C51 的 16 位 unsigned int,uchar 仍然是 8 位 unsigned char。
类型别名可以减少重复书写,但命名要有统一规则。如果工程或第三方库已经定义了同名 uint,再次定义可能产生冲突;大型或跨平台项目更适合使用含义明确的定宽类型。无论使用原类型名、typedef 别名还是 <stdint.h>,选择类型时都应先确认数值范围、符号需求和目标平台的真实位宽。
本节程序主体仍运行在 while(1) 无限循环中,持续扫描按键和光敏输入。后续引入中断后,中断服务函数会在主循环之外定义,并由硬件事件触发执行;主循环与中断之间共享的数据还需要考虑 volatile、原子访问和并发一致性。