第一次玩51单片机双机通信时,我用的是最基础的独立按键方案——甲机按一下按键发送一个数字,乙机用数码管显示。这种方案虽然简单直接,但实际用起来特别别扭。比如要发送数字9,就得连续按9次按键,操作体验堪比老式功能手机发短信。后来在项目评审时,有工程师建议改用矩阵键盘,这才打开了新世界的大门。
硬件改造其实比想象中简单。原先的独立按键电路只需要P1.7一个IO口,改成4x4矩阵键盘后,需要占用P1和P3两个端口的8个引脚。具体接线时,我把键盘的行线(ROW0-ROW3)接到P1.0-P1.3,列线(COL0-COL3)接到P3.0-P3.3。这里有个细节要注意:开发板上的P3.0和P3.1默认是串口通信引脚,接键盘时要避开这两个引脚,我后来改成了P3.2-P3.5。
矩阵键盘的扫描原理其实很巧妙。通过轮流给每列输出低电平,同时检测行线状态,就能定位到具体按下的按键。比如当P3.2输出低电平时,如果检测到P1.0为低电平,就说明第一行第一列的按键被按下。实际调试时发现个坑:键盘必须加10kΩ上拉电阻,否则会出现误触发。我在每个行线都加了上拉电阻后,按键识别就稳定多了。
提示:矩阵键盘的消抖处理比独立按键更复杂,建议在硬件上加0.1μF电容,软件里再加20ms延时检测
键盘扫描程序的核心是状态机思维。我最初写的扫描函数是这样的:直接轮询所有按键,检测到按下就立即发送键值。结果在实际运行中,经常出现长按按键时连续发送几十个相同数据的问题。后来改用状态标志位才解决这个问题,具体实现分三个状态:
c复制#define KEY_RELEASED 0
#define KEY_DETECTED 1
#define KEY_CONFIRMED 2
unsigned char keyState = KEY_RELEASED;
unsigned char keyValue;
void ScanKeyboard() {
static unsigned char lastKey = 0xFF;
unsigned char currentKey = GetKeyValue(); // 获取当前按键值
switch(keyState) {
case KEY_RELEASED:
if(currentKey != 0xFF) {
keyValue = currentKey;
keyState = KEY_DETECTED;
}
break;
case KEY_DETECTED:
Delay(20);
if(GetKeyValue() == keyValue) {
keyState = KEY_CONFIRMED;
UART_SendByte(keyValue);
} else {
keyState = KEY_RELEASED;
}
break;
case KEY_CONFIRMED:
if(GetKeyValue() == 0xFF) {
keyState = KEY_RELEASED;
}
break;
}
}
键值映射也是个需要仔细处理的地方。4x4键盘通常需要将行列坐标转换成0-F的十六进制值。我的做法是建立二维数组映射表:
c复制const unsigned char KeyMap[4][4] = {
{0x00, 0x01, 0x02, 0x03},
{0x04, 0x05, 0x06, 0x07},
{0x08, 0x09, 0x0A, 0x0B},
{0x0C, 0x0D, 0x0E, 0x0F}
};
原始的通信协议太简陋了——直接发送裸数据,没有任何校验机制。我在实际测试中发现,当传输距离超过1米时,偶尔会出现数据错误。于是对协议做了三点改进:
改进后的数据帧格式如下:
| 字段 | 帧头 | 数据长度 | 键值数据 | 校验和 | 帧尾 |
|---|---|---|---|---|---|
| 字节 | 0xAA | 1 | 1 | 1 | 0x55 |
对应的发送函数修改为:
c复制void SendKeyValue(unsigned char value) {
unsigned char buffer[5];
buffer[0] = 0xAA; // 帧头
buffer[1] = 0x01; // 数据长度
buffer[2] = value; // 键值
buffer[3] = ~(buffer[0]+buffer[1]+buffer[2]); // 校验和
buffer[4] = 0x55; // 帧尾
for(int i=0; i<5; i++) {
UART_SendByte(buffer[i]);
}
}
接收端也要相应修改中断服务程序:
c复制void UART_Routine() interrupt 4 {
static unsigned char state = 0;
static unsigned char data[5];
static unsigned char index = 0;
if(RI) {
RI = 0;
unsigned char byte = SBUF;
switch(state) {
case 0: // 等待帧头
if(byte == 0xAA) {
state = 1;
index = 0;
data[index++] = byte;
}
break;
case 1: // 接收数据
data[index++] = byte;
if(index >= 5) {
state = 2;
}
break;
case 2: // 校验数据
unsigned char sum = 0;
for(int i=0; i<4; i++) {
sum += data[i];
}
if((sum & 0xFF) == 0xFF && data[4] == 0x55) {
DisplayKeyValue(data[2]); // 显示有效数据
}
state = 0;
break;
}
}
}
第一次联调时遇到了几个典型问题,这里分享下解决方案:
问题1:键盘扫描导致通信卡顿
现象:快速按键时,乙机显示明显延迟
原因分析:键盘扫描函数占用太多CPU时间
解决方法:改用定时器中断扫描,每10ms扫描一次键盘
c复制void Timer0_Init() {
TMOD &= 0xF0;
TMOD |= 0x01;
TL0 = 0x00;
TH0 = 0xDC;
ET0 = 1;
TR0 = 1;
}
void Timer0_Routine() interrupt 1 {
static unsigned char counter = 0;
TL0 = 0x00;
TH0 = 0xDC;
if(++counter >= 10) { // 10ms定时
counter = 0;
ScanKeyboard();
}
}
问题2:长距离传输数据错误
现象:当两台单片机距离超过2米时,误码率明显上升
解决方法:
问题3:多按键同时按下时的冲突
现象:同时按两个键时,有时会识别成第三个键值
解决方案:在键盘扫描函数中加入防冲突逻辑,检测到多键按下时视为无效输入
c复制unsigned char GetKeyValue() {
unsigned char row, col;
unsigned char key = 0xFF;
unsigned char count = 0;
for(col=0; col<4; col++) {
P3 = ~(1 << col);
row = P1 & 0x0F;
if(row != 0x0F) {
for(int i=0; i<4; i++) {
if(!(row & (1<<i))) {
if(++count > 1) return 0xFF; // 多键按下
key = KeyMap[i][col];
}
}
}
}
return key;
}
完成基础功能后,可以尝试以下几个扩展方向:
扩展1:增加LCD显示
在乙机端添加1602液晶,除了数码管显示外,还能实时打印通信状态和接收到的数据。需要修改接收程序:
c复制void DisplayKeyValue(unsigned char value) {
P2 = DSY_CODE[value & 0x0F]; // 数码管显示
LCD_ShowHex(1, 8, value); // LCD显示十六进制值
}
扩展2:实现双向通信
让甲乙两机都能发送和接收数据,需要修改硬件连接:
扩展3:无线通信改造
用蓝牙模块(如HC-05)替代有线连接:
c复制void Bluetooth_Init() {
PCON &= 0x7F;
SCON = 0x50;
TMOD &= 0x0F;
TMOD |= 0x20;
TL1 = 0xFA; // 38400bps@11.0592MHz
TH1 = 0xFA;
ET1 = 0;
TR1 = 1;
}
扩展4:增加上位机监控
通过USB转串口模块连接电脑,用串口助手软件监控通信数据。可以进一步用Python编写简单的监控程序:
python复制import serial
from datetime import datetime
ser = serial.Serial('COM3', 9600, timeout=1)
log_file = open('comm_log.txt', 'a')
while True:
data = ser.read(5)
if len(data) == 5 and data[0] == 0xAA and data[4] == 0x55:
timestamp = datetime.now().strftime("%H:%M:%S")
log_file.write(f"[{timestamp}] Key: {hex(data[2])}\n")
log_file.flush()