关键技巧
使用static变量保存状态,只在状态变化时更新锁存器,有效避免影响数码管显示,节省CPU资源
基于STC15F2K60S2的完整驱动模板构建教程,包含12个核心模块:锁存器、LED、键盘、数码管、时钟、温度、IIC、串口、超声波、NE555测频等
理解锁存器、三八译码器和延时函数的工作原理
通过P2的高三位(P25/P26/P27)控制Y4C-Y7C四个锁存器,实现对不同外设的独立控制
使用intrins.h中的_nop_()函数实现微秒级精确延时,用于各种通信时序控制
锁存器操作的标准写法,确保高三位正确控制且不影响其他位
P2 = P2 & 0x1f | 0x80; // 打开Y4C
P2 &= 0x1f; // 关闭锁存器
掌握基础输出设备的控制方法与状态保持技巧
/// @brief LED扫描控制(带状态保持) /// @param addr LED地址 0-7 对应 L1-L8 /// @param enable 0-熄灭 1-点亮 void Led_Disp(unsigned char addr, unsigned char enable) { static unsigned char temp = 0x00; // 当前LED状态 static unsigned char temp_old = 0xff; // 上一次状态 if (enable) temp |= 0x01 << addr; // 置位对应位 else temp &= ~(0x01 << addr); // 清零对应位 if (temp != temp_old) { // 状态变化才更新(避免闪烁) P0 = ~temp; // 取反输出(共阳接法,1灭0亮) P2 = P2 & 0x1f | 0x80; // Y4C锁存器打开 P2 &= 0x1f; // 关闭锁存器 temp_old = temp; } }
使用static变量保存状态,只在状态变化时更新锁存器,有效避免影响数码管显示,节省CPU资源
unsigned char temp_0 = 0x00; unsigned char temp_old_0 = 0xff; void Beep(bit enable) { if (enable) temp_0 |= 0x40; // BUZZ对应位 0100_0000 = 0x40 else temp_0 &= ~(0x40); if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 & 0x1f | 0xa0; // Y5C锁存器 P2 &= 0x1f; temp_old_0 = temp_0; } }
void Relay(bit enable) { if (enable) temp_0 |= 0x10; // RELAY对应位 0001_0000 = 0x10 else temp_0 &= ~(0x10); if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 & 0x1f | 0xa0; P2 &= 0x1f; temp_old_0 = temp_0; } }
void MOTOR(bit enable) { if (enable) temp_0 |= 0x20; // MOTOR对应位 0010_0000 = 0x20 else temp_0 &= ~(0x20); if (temp_0 != temp_old_0) { P0 = temp_0; P2 = P2 & 0x1f | 0xa0; P2 &= 0x1f; temp_old_0 = temp_0; } }
| 设备 | 对应引脚 | 控制值 | 锁存器 |
|---|---|---|---|
| LED (L1-L8) | Q0-Q7 | 0x80 | Y4C |
| RELAY (L10) | Q4 | 0x10 | Y5C (0xA0) |
| MOTOR | Q5 | 0x20 | |
| BUZZ (蜂鸣器) | Q6 | 0x40 |
4×3矩阵键盘扫描 + 精妙的三行代码消抖原理
unsigned char Key_Read() { unsigned char temp = 0; // 关闭定时器1中断,避免与串口冲突 ET1 = 0; // 第一列 (P44=0) P44 = 0; P42 = 1; P35 = 1; if (P33 == 0) temp = 4; if (P32 == 0) temp = 5; if (P31 == 0) temp = 6; if (P30 == 0) temp = 7; // 第二列 (P42=0) P44 = 1; P42 = 0; P35 = 1; if (P33 == 0) temp = 8; if (P32 == 0) temp = 9; if (P31 == 0) temp = 10; if (P30 == 0) temp = 11; // 第三列 (P35=0) - 已删除P34避免NE555冲突 P44 = 1; P42 = 1; P35 = 0; if (P33 == 0) temp = 12; if (P32 == 0) temp = 13; if (P31 == 0) temp = 14; if (P30 == 0) temp = 15; // 恢复P3口并开启中断 P3 = 0xff; ET1 = 1; return temp; }
void Key_Proc() { static unsigned char Key_Val, Key_Down, Key_Up, Key_Old; if (time_all_1s % 10) return; // 10ms执行一次 Key_Val = Key_Read(); // 读取当前值 Key_Down = Key_Val & (Key_Old ^ Key_Val); // 按下检测 Key_Up = ~Key_Val & (Key_Old ^ Key_Val); // 抬起检测 Key_Old = Key_Val; // 保存旧值 // 使用示例 if (Key_Down == 4) { // S4按下执行的代码 } }
| 键盘状态 | Key_Old | Key_Val | Key_Old^Key_Val | Key_Down | Key_Up |
|---|---|---|---|---|---|
| 未按下 | 0000 | 0000 | 0000 | 0000 | 0000 |
| 按下过程中 | 0000 | 0100 | 0100 | 0100 | 0000 |
| 按下稳定 | 0100 | 0100 | 0000 | 0000 | 0000 |
| 抬起过程中 | 0100 | 0000 | 0100 | 0000 | 0100 |
关键:Key_Down只在按下瞬间为按键值,其余时刻为0;Key_Up只在抬起瞬间为按键值
省赛和国赛都考了NE555测量,使用P34引脚,与键盘第四列冲突。建议直接使用4×3键盘布局(删除P34相关代码)。
共阳极数码管的位选、段选与消隐处理
共阳极数码管:0亮1灭,按照 dp-g-f-e-d-c-b-a 顺序编码
位选切换前先将段码置0xFF,避免显示残影和鬼影现象
小数点对应DP段(最高位),通过 &0x7F 实现点亮
if(point) P0 &= 0x7F; // 0x7F = 0111_1111
// 段码表:0-9 + 灭(10) + A(11) code unsigned char seg_dula[] = { 0xc0, 0xf9, 0xa4, 0xb0, 0x99, // 0-4 0x92, 0x82, 0xf8, 0x80, 0x90, // 5-9 0xff, 0x88 // 灭, A }; /// @brief 数码管显示函数 /// @param wela 位选 0-7(对应COM1-8) /// @param dula 段选 0-9(对应显示数字) /// @param point 小数点 0-无 1-有 void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point) { // 1. 消隐(关键!避免鬼影) P0 = 0xff; P2 = P2 & 0x1f | 0xe0; // Y7C - 段选锁存器 P2 &= 0x1f; // 2. 位选 P0 = 0x01 << wela; // 选择第wela位 P2 = P2 & 0x1f | 0xc0; // Y6C - 位选锁存器 P2 &= 0x1f; // 3. 段选 P0 = seg_dula[dula]; if (point) P0 &= 0x7f; // 显示小数点(DP置0) P2 = P2 & 0x1f | 0xe0; // Y7C P2 &= 0x1f; }
实时时钟芯片的读写与BCD码转换
| 操作 | 小时 | 分钟 | 秒钟 | 写保护 |
|---|---|---|---|---|
| 读取 | 0x85 | 0x83 | 0x81 | - |
| 写入 | 0x84 | 0x82 | 0x80 | 0x8E |
写入前需将0x8E的WP位置0解除写保护,写入后置1开启保护
十进制 → BCD: 23 → 2,3 → 0010,0011 → 0x23
temp = (ucRtc[i] / 10) << 4 | (ucRtc[i] % 10);
BCD → 十进制: 0x23 → 0x2,0x3 → 2*10+3 → 23
ucRtc[i] = (temp >> 4) * 10 + (temp & 0x0f);
// 引脚定义 sbit SDA = P2 ^ 3; sbit RST = P1 ^ 3; sbit SCK = P1 ^ 7; /// @brief 写入时间(十进制输入,自动转BCD) /// @param ucRtc 时间数组 [时,分,秒] void Set_Rtc(unsigned char *ucRtc) { unsigned char i, temp; Write_Ds1302_Byte(0x8e, 0x00); // 解除写保护 for (i = 0; i < 3; i++) { // 十进制转BCD: 23 -> 0x23 temp = (ucRtc[i] / 10) << 4 | (ucRtc[i] % 10); Write_Ds1302_Byte(0x84 - 2 * i, temp); } Write_Ds1302_Byte(0x8e, 0x80); // 开启写保护 } /// @brief 读取时间(BCD转十进制) void Read_Rtc(unsigned char *ucRtc) { unsigned char i, temp; for (i = 0; i < 3; i++) { temp = Read_Ds1302_Byte(0x85 - 2 * i); // BCD转十进制: 0x23 -> 23 ucRtc[i] = (temp >> 4) * 10 + (temp & 0x0f); } }
unsigned char time[3] = {11, 12, 13}; // 时:分:秒
Set_Rtc(time); // 设置时间
Read_Rtc(time); // 读取时间
DS18B20单总线温度采集与精度配置
L = 340m/s × T / 2 = 0.017 × time(us)
return (0.017 * time + 3);
sbit DQ = P1 ^ 4; /// @brief 读取DS18B20温度值 /// @return 温度值(浮点数,单位:°C) float rd_temperature() { unsigned char low, high; // 1. 启动温度转换 init_ds18b20(); Write_DS18B20(0xcc); // 跳过ROM Write_DS18B20(0x44); // 启动转换 Delay_OneWire(200); // 等待转换完成(必须延时) // 2. 读取温度值 init_ds18b20(); Write_DS18B20(0xcc); // 跳过ROM Write_DS18B20(0xbe); // 读取暂存器 low = Read_DS18B20(); // 低位字节 high = Read_DS18B20(); // 高位字节 // 合成温度值并乘以精度(12位默认0.0625) return (float)(high << 8 | low) * 0.0625; }
PCF8591(AD/DA)与AT24C02(EEPROM)的IIC操作
/// @brief PCF8591 AD采样 /// @param addr 通道地址 0x00-0x03 /// @return 采样值 0-255 unsigned char Ad_Read(unsigned char addr) { unsigned char temp; I2CStart(); I2CSendByte(0x90); // PCF8591写地址 I2CWaitAck(); I2CSendByte(addr); // 选择通道 I2CWaitAck(); I2CStart(); // 重复起始 I2CSendByte(0x91); // PCF8591读地址 I2CWaitAck(); temp = I2CReceiveByte(); // 读取数据 I2CSendAck(1); // 发送非应答(结束) I2CStop(); return temp; } // 常用地址配置: // 0x01 - 光敏电阻 (AIN1) // 0x03 - 滑动变阻器 (AIN3) // 0x41 - 开启DA输出的光敏通道
/// @brief PCF8591 DA输出 /// @param dat 数字值 0-255 对应 0-5V void Da_Write(unsigned char dat) { I2CStart(); I2CSendByte(0x90); // PCF8591写地址 I2CWaitAck(); I2CSendByte(0x41); // 开启DA输出 + AIN1通道 I2CWaitAck(); I2CSendByte(dat); // 发送数字值 I2CWaitAck(); I2CStop(); } // 电压换算:数字值 = 电压(V) × 51 // 例如:输出2V -> Da_Write(2 * 51) // 例如:输出2.5V -> Da_Write(2.5 * 51)
/// @brief AT24C02 读取数据 /// @param str 数据存储数组 /// @param addr EEPROM内部地址 /// @param num 读取字节数 void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) { I2CStart(); I2CSendByte(0xa0); // AT24C02写地址 I2CWaitAck(); I2CSendByte(addr); // 设置读取地址 I2CWaitAck(); I2CStart(); I2CSendByte(0xa1); // AT24C02读地址 I2CWaitAck(); while (num--) { *str++ = I2CReceiveByte(); if (num) I2CSendAck(0); // 继续读取 else I2CSendAck(1); // 读取完成 } I2CStop(); }
/// @brief AT24C02 写入数据 /// @param str 数据数组 /// @param addr EEPROM内部地址 /// @param num 写入字节数 void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) { I2CStart(); I2CSendByte(0xa0); // AT24C02写地址 I2CWaitAck(); I2CSendByte(addr); // 设置写入地址 I2CWaitAck(); while (num--) { I2CSendByte(*str++); I2CWaitAck(); I2C_Delay(200); // 写入延时 } I2CStop(); // 必须延时等待写入完成(重要!) I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); }
使用定时器2的串口初始化与printf重定向
通过重载putchar函数,将printf输出重定向到串口
/// @brief 串口1初始化 9600bps@12.000MHz void Uart1_Init(void) { SCON = 0x50; // 8位数据,可变波特率 AUXR |= 0x01; // 串口1选择定时器2 AUXR |= 0x04; // 定时器时钟1T模式 T2L = 0xC7; // 定时器初值低8位 T2H = 0xFE; // 定时器初值高8位 AUXR |= 0x10; // 定时器2开始计时 ES = 1; // 使能串口1中断 EA = 1; // 开启总中断 } /// @brief putchar重定向,支持printf char putchar(char ch) { SBUF = ch; // 发送数据 while (!TI); // 等待发送完成 TI = 0; // 清除标志 return ch; }
串口使用P30(RXD)和P31(TXD)引脚,与键盘扫描存在硬件冲突。在Key_Read()函数中需要关闭定时器1中断(ET1=0)并在结束后恢复(ET1=1)。
PCA定时器实现超声波发送与接收测距
发送8个40kHz方波,测量回波时间,计算距离
使用PCA模块计时,节省定时器资源
sbit Tx = P1 ^ 0; // 发送引脚 sbit Rx = P1 ^ 1; // 接收引脚 // 12us延时(ISP生成,可调整为38) void Delay12us(void) { unsigned char i; _nop_(); _nop_(); i = 33; // 可修改为38 while (--i); } // 发送8个40kHz方波 void Ut_Wave_Init() { unsigned char i; for (i = 0; i < 8; i++) { Tx = 1; Delay12us(); Tx = 0; Delay12us(); } } /// @brief 超声波测距 /// @return 距离值(cm),0表示测量错误 unsigned char Ut_Wave_Data() { unsigned int time; CH = CL = 0; // 清零计数器 CMOD = 0x00; // 16位不重载 EA = 0; Ut_Wave_Init(); // 发送超声波 EA = 1; CR = 1; // 开始计时 while (Rx && !CF); // 等待接收或溢出 CR = 0; // 停止计时 if (!CF) { // 正常接收 time = CH << 8 | CL; return (0.017 * time + 3); // 距离计算 } else { // 溢出错误 CF = 0; return 0; } }
定时器0计数 + 定时器1定时 = 精准测频
定时器0作为计数器,定时器1每秒读取一次计数值
P34引脚与SIGNAL跳线帽连接
/// @brief 定时器0初始化 - 用于NE555计数 void Timer0_Init(void) { AUXR &= 0x7F; // 定时器时钟12T模式 TMOD &= 0xF0; // 清除T0配置 TMOD |= 0x05; // T0设置为16位计数模式 TL0 = 0; // 计数初值低8位 TH0 = 0; // 计数初值高8位 TF0 = 0; // 清除TF0标志 TR0 = 1; // 定时器0开始计数 EA = 1; } /// @brief 定时器1中断 - 1秒读取频率 void Timer1_Isr(void) interrupt 3 { if (++time_all_1s == 1000) { // 1秒到达 time_all_1s = 0; Freq = (TH0 << 8) | TL0; // 读取计数值 TH0 = TL0 = 0; // 清零计数器 } // ... 其他中断处理 }
模块化编程与定时器中断调度完整示例
#include "main.h" /* ==================== 全局变量定义 ==================== */ unsigned char ucLed[8] = {0}; unsigned char Seg_Pos; unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10}; unsigned char Seg_Point[8] = {0}; unsigned char Uart_Buf[10]; unsigned char Uart_Rx_Index; bit Uart_flag; unsigned char Sys_Tick; unsigned char ucRtc[3] = {11,11,11}; unsigned int time_all_1s; unsigned int Freq; /* ==================== 数据读取模块 ==================== */ void Data_Proc() { if (time_all_1s % 50 == 0) { Read_Rtc(ucRtc); // 50ms读取时间 } if (time_all_1s % 100 == 0) { // 100ms读取AD } if (time_all_1s % 500 == 0) { // 500ms读取温度 } } /* ==================== 键盘处理 ==================== */ void Key_Proc() { static unsigned char Key_Val, Key_Down, Key_Up, Key_Old; if (time_all_1s % 10) return; Key_Val = Key_Read(); Key_Down = Key_Val & (Key_Old ^ Key_Val); Key_Up = ~Key_Val & (Key_Old ^ Key_Val); Key_Old = Key_Val; if (Key_Down == 4) { // S4按下处理 } } /* ==================== 数码管处理 ==================== */ void Seg_Proc() { if (time_all_1s % 20) return; // 界面切换与显示更新 } /* ==================== 定时器1中断 ==================== */ void Timer1_Isr(void) interrupt 3 { unsigned char i; if (++time_all_1s == 1000) { time_all_1s = 0; Freq = (TH0 << 8) | TL0; // 读取NE555频率 TH0 = TL0 = 0; } if (Uart_flag) Sys_Tick++; // 串口超时计数 Seg_Pos = (++Seg_Pos) % 8; // 数码管位选 Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]); for (i = 0; i < 8; i++) Led_Disp(i, ucLed[i]); // LED扫描 } /* ==================== 串口中断 ==================== */ void Uart1_Isr(void) interrupt 4 { if (RI) { Uart_flag = 1; Sys_Tick = 0; Uart_Buf[Uart_Rx_Index++] = SBUF; RI = 0; } if (Uart_Rx_Index > 10) Uart_Rx_Index = 0; } /* ==================== 主函数 ==================== */ void main() { System_Init(); // 系统初始化 Timer0_Init(); // NE555计数器 Timer1_Init(); // 轮询定时器 Set_Rtc(ucRtc); rd_temperature(); Delay750ms(); while (1) { Data_Proc(); Key_Proc(); Seg_Proc(); Led_Proc(); } }
采用轮询方式,通过定时器产生不同时间间隔的标志位,在main循环中调度各个功能模块。数码管和LED在中断中扫描,保证显示稳定;业务逻辑在主循环中处理,避免中断中执行耗时操作。