本帖最后由 云天 于 2026-2-22 11:35 编辑
引言 在智能手机普及的今天,我们或许已经很久没有体验过传统电话机那“按键输入、等待接通”的仪式感了。为了重温这种质朴的通话体验,同时探索现代4G通信模块的应用,我制作了这台简易模拟电话机。它不需要固定电话线,只需插入SIM卡,就能像普通手机一样拨打电话,但操作方式却像老式座机——通过物理键盘输入号码,OLED屏幕显示信息,整个过程简单而纯粹。 本项目使用的4G通信模块是DFrobot的CAT4:SIM7600G-H 4G 通信模块 支持数据通讯、语音通话、短信收发、USB网卡、GNSS卫星定位的全球频段4G CAT4无线通讯模组。 功能概览拨打电话:通过4×4键盘输入号码,按“#”键拨打。 接听电话:来电时屏幕显示号码,按“*”键拒接(挂断)。 号码显示:OLED屏幕实时显示输入号码、来电号码及通话状态(待机、拨号中、来电、通话中)。 音频输出:通过SIM7600模块的喇叭接口输出通话声音,无需耳机。 挂断与清除:通话中按“”挂断;待机时按“”清除已输入号码。
硬件清单
| | | | 主控板 | FireBeetle 2 ESP32-E (DFR0654) | 负责逻辑控制、键盘扫描、屏幕驱动及与4G模块通信。 | | 4G通信模块 | SIM7600G-H (TEL0162) | 实现4G网络注册、语音通话功能。 | | 键盘 | 4×4薄膜数字键盘 | 输入电话号码及功能指令。 | | OLED显示屏 | 0.91寸 128×32 I2C OLED (DFR0647) | 显示号码和状态信息。 | | 喇叭 | 8Ω 1W 喇叭 | 接在SIM7600模块的音频输出接口,用于通话声音输出。 | | 电源 | 5V/3A 外部电源 | 独立为SIM7600模块供电(必须!)。 | | 辅助 | 杜邦线、面包板等 | 连接电路。 |
硬件连接
1. SIM7600G-H 与 ESP32 连接(使用 UART2)
SIM7600G-H | ESP32 (FireBeetle) | | TXD | IO16 (RX2) | | RXD | IO17 (TX2) | | GND | GND | | VCC/5V | 外部5V电源正极 | | GND (电源) | 外部5V电源负极 |
重要:SIM7600模块必须使用独立外部电源供电(5V/2A~3A),不可从ESP32取电!同时将外部电源的GND与ESP32的GND相连,保证共地。 2. 4×4 键盘连接 键盘为8引脚矩阵,行引脚接ESP32的数字IO,列引脚也接数字IO。本例接线如下(可根据实际调整):
键盘引脚 | ESP32引脚 | | R1 | IO15 | | R2 | IO13 | | R3 | IO14 | | R4 | IO2 | | C1 | IO26 | | C2 | IO25 | | C3 | IO12 | | C4 | IO4 |
3. OLED 显示屏连接(I2C)
OLED | ESP32 | | VCC | 3.3V | | GND | GND | | SCL | IO22 | | SDA | IO21 |
软件实现
开发环境
代码结构程序主要包含以下部分: 初始化:串口、键盘、OLED、SIM7600模块。 键盘扫描:在 loop() 中轮询按键,处理数字累积、拨号、挂断。 AT指令处理:通过串口2与SIM7600通信,发送指令并解析响应。 状态机:用 callStatus 变量记录当前状态(IDLE, DIALING, RINGING, ACTIVE),并据此更新屏幕。 OLED显示:自定义 updateDisplay() 函数,根据状态显示不同内容。
关键代码片段初始化部分
- #include <HardwareSerial.h>
- #include <Keypad.h>
- #include <U8g2lib.h>
-
- HardwareSerial sim7600(2); // UART2
- U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
-
- // 键盘配置
- const byte ROWS = 4, COLS = 4;
- char keys[ROWS][COLS] = {
- {'1','2','3','A'},
- {'4','5','6','B'},
- {'7','8','9','C'},
- {'*','0','#','D'}
- };
- byte rowPins[ROWS] = {15,13,14,2};
- byte colPins[COLS] = {26,25,12,4};
- Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
-
- String phoneNumber = "";
- String incomingNumber = "";
- String callStatus = "IDLE";
-
- void setup() {
- Serial.begin(115200);
- sim7600.begin(115200, SERIAL_8N1, 16, 17);
- u8g2.begin();
-
- // 初始化SIM7600
- sendCommand("AT", 1000);
- sendCommand("AT+CPIN?", 1000);
- sendCommand("AT+CREG?", 1000);
- sendCommand("AT+CSQ", 1000);
- sendCommand("AT+CSDVC=3", 1000); // 喇叭输出
- sendCommand("AT+CLIP=1", 1000); // 来电显示
-
- updateDisplay();
- }
复制代码 通话状态检测(处理模块上报)
- while (sim7600.available()) {
- String response = sim7600.readString();
- Serial.print("模块上报: ");
- Serial.println(response);
-
- if (response.indexOf("RING") >= 0) {
- callStatus = "RINGING";
- // 提取来电号码...
- updateDisplay();
- }
- else if (response.indexOf("CONNECT") >= 0) {
- callStatus = "ACTIVE";
- updateDisplay();
- }
- else if (response.indexOf("VOICE CALL: END") >= 0 ||
- response.indexOf("NO CARRIER") >= 0) {
- callStatus = "IDLE";
- phoneNumber = "";
- incomingNumber = "";
- updateDisplay();
- }
- }
复制代码 OLED显示函数(英文版)
- void updateDisplay() {
- u8g2.firstPage();
- do {
- u8g2.setFont(u8g2_font_6x10_tf);
- u8g2.setFontPosTop();
-
- // 状态行
- String statusLine = "Status: ";
- if (callStatus == "IDLE") statusLine += "IDLE";
- else if (callStatus == "DIALING") statusLine += "DIALING...";
- else if (callStatus == "RINGING") statusLine += "INCOMING!";
- else if (callStatus == "ACTIVE") statusLine += "ACTIVE";
- u8g2.drawStr(0, 0, statusLine.c_str());
-
- // 号码行
- u8g2.setCursor(0, 12);
- if (callStatus == "RINGING") {
- u8g2.print("From:");
- u8g2.setCursor(35, 12);
- u8g2.print(incomingNumber.c_str());
- } else if (callStatus == "DIALING" || callStatus == "ACTIVE") {
- u8g2.print("To:");
- u8g2.setCursor(35, 12);
- u8g2.print(phoneNumber.c_str());
- } else {
- u8g2.print("Number:");
- u8g2.setCursor(45, 12);
- u8g2.print(phoneNumber.c_str());
- }
-
- // 提示行
- u8g2.setCursor(0, 24);
- if (callStatus == "IDLE") {
- u8g2.print("# Dial * Clear");
- } else if (callStatus == "RINGING") {
- u8g2.print("* Reject");
- } else if (callStatus == "ACTIVE") {
- u8g2.print("* Hang up");
- } else {
- u8g2.print("Please wait...");
- }
- } while (u8g2.nextPage());
- }
复制代码 完整代码
- #include <HardwareSerial.h>
- #include <Keypad.h>// 需要安装Keypad库
- #include <Wire.h>
- #include <U8g2lib.h> // 需要安装U8g2库
-
- // --- OLED 配置 (使用U8g2库) ---
- // 构造函数:硬件I2C,地址默认为0x3C,复位引脚设为无
- U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
-
- // --- SIM7600 配置 ---
- HardwareSerial sim7600(2);
- //const int PWRKEY_PIN = 4; // 如果不需要控制开机,可注释掉
-
- // --- 键盘配置 (与你实际连接一致) ---
- const byte ROWS = 4;
- const byte COLS = 4;
- char keys[ROWS][COLS] = {
- {'1','2','3','A'},
- {'4','5','6','B'},
- {'7','8','9','C'},
- {'*','0','#','D'}
- };
- // **请务必根据你的实际接线修改引脚号!**
- byte rowPins[ROWS] = {15, 13, 14, 2};
- byte colPins[COLS] = {26, 25, 12, 4};
- Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
-
- // --- 全局变量 ---
- String phoneNumber = ""; // 存储用户输入的号码
- String incomingNumber = ""; // 存储来电号码
- bool isCallActive = false; // 通话状态标志
- String callStatus = "IDLE"; // 状态: IDLE, DIALING, ACTIVE, RINGING
-
- void setup() {
- Serial.begin(115200);
- delay(1000);
- Serial.println("ESP32 + 键盘拨号 + OLED 示例启动...");
-
- // --- 初始化OLED ---
- Wire.begin(); // 初始化I2C
- u8g2.begin();
- u8g2.enableUTF8Print(); // 启用UTF8支持,便于显示特殊符号
- updateDisplay(); // 立即更新一次屏幕显示
-
- // --- 初始化SIM7600 ---
- sim7600.begin(115200, SERIAL_8N1, 16, 17);
- // 如果模块需要PWRKEY引脚触发开机,取消下面的注释
- // pinMode(PWRKEY_PIN, OUTPUT);
- // digitalWrite(PWRKEY_PIN, LOW); delay(1000); digitalWrite(PWRKEY_PIN, HIGH); delay(3000);
-
- // 发送基础AT指令
- sendCommand("AT", 1000);
- sendCommand("AT+CPIN?", 1000);
- sendCommand("AT+CREG?", 1000);
- sendCommand("AT+CSQ", 1000);
-
- // 配置音频为喇叭
- sendCommand("AT+CSDVC=3", 1000);
- sendCommand("AT+CLVL=2", 1000);
- // 开启来电显示
- sendCommand("AT+CLIP=1", 1000);
-
- Serial.println("初始化完成。请使用键盘...");
- callStatus = "IDLE";
- updateDisplay();
- }
-
- void loop() {
- // --- 1. 处理键盘输入 (与之前相同) ---
- char key = keypad.getKey();
- if (key) {
- Serial.print("按键: ");
- Serial.println(key);
-
- if ((key >= '0' && key <= '9') || key == 'A' || key == 'B' || key == 'C' || key == 'D') {
- if (callStatus == "IDLE") { // 仅在待机时允许输入
- phoneNumber += key;
- Serial.print("当前号码: ");
- Serial.println(phoneNumber);
- updateDisplay(); // **每次输入后更新屏幕**
- } else {
- Serial.println("通话中,不能输入");
- }
- }
- else if (key == '#') {
- if (callStatus == "IDLE") {
- if (phoneNumber.length() > 0) {
- makePhoneCall(phoneNumber);
- callStatus = "DIALING"; // 状态变为拨号中
- updateDisplay();
- } else {
- Serial.println("请输入号码");
- }
- } else {
- Serial.println("通话中,不能拨号");
- }
- }
- else if (key == '*') {
- if (callStatus == "ACTIVE" || callStatus == "DIALING" || callStatus == "RINGING") {
- hangupCall();
- // 状态更新将在模块上报 "NO CARRIER" 时进行
- } else {
- phoneNumber = "";
- incomingNumber = "";
- Serial.println("输入已清除");
- updateDisplay();
- }
- }
- }
-
- // --- 2. 处理SIM7600主动上报的消息 ---
- while (sim7600.available()) {
- String response = sim7600.readString();
- Serial.print("模块上报: ");
- Serial.println(response);
-
- // 检测来电
- if (response.indexOf("RING") >= 0) {
- callStatus = "RINGING";
- // 提取来电号码...
- int start = response.indexOf('"') + 1;
- int end = response.indexOf('"', start);
- if (start > 0 && end > start) {
- incomingNumber = response.substring(start, end);
- } else {
- incomingNumber = "Unknown";
- }
- updateDisplay();
- }
- // 检测通话接通
- else if (response.indexOf("CONNECT") >= 0) {
- callStatus = "ACTIVE";
- updateDisplay();
- }
- // 检测通话结束(新增对 VOICE CALL: END 的判断)
- else if (response.indexOf("NO CARRIER") >= 0 ||
- response.indexOf("BUSY") >= 0 ||
- response.indexOf("NO ANSWER") >= 0 ||
- response.indexOf("VOICE CALL: END") >= 0) { // ← 关键添加
- callStatus = "IDLE";
- phoneNumber = "";
- incomingNumber = "";
- updateDisplay();
- Serial.println("通话结束,屏幕已复位");
- }
- }
- }
-
- // --- 辅助函数 ---
- void sendCommand(String cmd, int delayTime) {
- sim7600.println(cmd);
- delay(delayTime);
- while (sim7600.available()) {
- String response = sim7600.readString();
- Serial.println(response);
- }
- }
-
- void makePhoneCall(String number) {
- Serial.print("正在拨号: ");
- Serial.println(number);
- String command = "ATD" + number + ";";
- sim7600.println(command);
- delay(3000);
- // 状态由模块上报的 CONNECT/NO CARRIER 更新
- }
-
- void hangupCall() {
- Serial.println("挂断电话...");
- sim7600.println("AT+CHUP");
- delay(1000);
- // 状态由模块上报的 NO CARRIER 更新
- // 主动重置状态和屏幕
- callStatus = "IDLE";
- phoneNumber = "";
- incomingNumber = "";
- updateDisplay();
- }
-
- // --- OLED 显示函数 ---
- void updateDisplay() {
- u8g2.firstPage();
- do {
- // 使用英文字体(简单、省内存)
- u8g2.setFont(u8g2_font_6x10_tf);
- u8g2.setFontPosTop();
-
- // 第一行:状态 (英文)
- String statusLine = "Status: ";
- if (callStatus == "IDLE") statusLine += "IDLE";
- else if (callStatus == "DIALING") statusLine += "DIALING...";
- else if (callStatus == "RINGING") statusLine += "INCOMING!";
- else if (callStatus == "ACTIVE") statusLine += "ACTIVE";
- else statusLine += callStatus;
- u8g2.drawStr(0, 0, statusLine.c_str());
-
- // 第二行:号码信息 (英文标签)
- u8g2.setCursor(0, 12);
- if (callStatus == "RINGING") {
- u8g2.print("From:");
- u8g2.setCursor(35, 12);
- u8g2.print(incomingNumber.c_str());
- } else if (callStatus == "DIALING" || callStatus == "ACTIVE") {
- u8g2.print("To:");
- u8g2.setCursor(35, 12);
- u8g2.print(phoneNumber.c_str());
- } else { // IDLE
- u8g2.print("Number:");
- u8g2.setCursor(45, 12); // 调整光标位置以对齐
- u8g2.print(phoneNumber.c_str());
- }
-
- // 第三行:提示 (英文)
- u8g2.setCursor(0, 24);
- if (callStatus == "IDLE") {
- u8g2.print("# Dial * Clear");
- } else if (callStatus == "RINGING") {
- u8g2.print("* Reject");
- } else if (callStatus == "ACTIVE") {
- u8g2.print("* Hang up");
- } else {
- u8g2.print("Please wait...");
- }
-
- } while ( u8g2.nextPage() );
- }
复制代码
制作过程与问题解决
1. 电源是关键 SIM7600模块在通话时电流可达2A,起初尝试从ESP32的5V引脚取电,导致模块频繁重启。必须使用独立的外部5V电源,并共地。 2. 通话结束检测 挂断电话后,模块返回的是 VOICE CALL: END: 000013 而非标准的 NO CARRIER。通过增加对 "VOICE CALL: END" 的检测,成功让屏幕复位。 3. 汉字乱码 原计划用中文显示状态,但普通英文字体不支持中文。最终改用全英文显示,避免了字库加载的麻烦,也节省了内存。 4. 键盘响应 4×4键盘的行列引脚必须正确映射,否则按键会错乱。可根据键盘背面的内部连线图调整 rowPins 和 colPins 的顺序。 最终效果待机状态:屏幕显示“Status: IDLE”,下方提示“# Dial * Clear”。 输入号码:按下数字键,号码实时显示在“Number:”后面。 拨号:按“#”后状态变为“DIALING...”,等待接通。 通话中:状态变为“ACTIVE”,显示对方号码。 来电:状态变为“INCOMING!”,显示来电号码,按“*”拒接。 挂断:通话结束,屏幕自动恢复待机状态。
演示视频
总结与展望 这个项目将复古的按键操作与现代4G通信技术结合,打造了一个简单有趣的通信终端。通过亲手搭建,我不仅熟悉了ESP32的编程、AT指令集的使用,还学会了处理硬件供电、串口通信等实际问题。未来,我计划将它应用到智能拐杖项目中,实现老人跌倒后,自动拨打电话求助,并通过短信功能发送位置信息(本项目使用的4G模块支持GNSS卫星定位(GPS、CLONASS、BD))。 如果你也想制作一个这样的电话机,欢迎参考我的代码和接线,一起享受创造的乐趣!
|