12浏览
查看: 12|回复: 0

[项目] 复古与科技的碰撞:基于ESP32和4G模块的简易模拟电话机

[复制链接]
本帖最后由 云天 于 2026-2-22 11:35 编辑

引言
       在智能手机普及的今天,我们或许已经很久没有体验过传统电话机那“按键输入、等待接通”的仪式感了。为了重温这种质朴的通话体验,同时探索现代4G通信模块的应用,我制作了这台简易模拟电话机。它不需要固定电话线,只需插入SIM卡,就能像普通手机一样拨打电话,但操作方式却像老式座机——通过物理键盘输入号码,OLED屏幕显示信息,整个过程简单而纯粹。
       本项目使用的4G通信模块是DFrobot的CAT4:SIM7600G-H 4G 通信模块 支持数据通讯、语音通话、短信收发、USB网卡、GNSS卫星定位的全球频段4G CAT4无线通讯模组。
复古与科技的碰撞:基于ESP32和4G模块的简易模拟电话机图1
功能概览
  • 拨打电话:通过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)
TXDIO16 (RX2)
RXDIO17 (TX2)
GNDGND
VCC/5V外部5V电源正极
GND (电源)外部5V电源负极

       重要:SIM7600模块必须使用独立外部电源供电(5V/2A~3A),不可从ESP32取电!同时将外部电源的GND与ESP32的GND相连,保证共地。
       2. 4×4 键盘连接
       键盘为8引脚矩阵,行引脚接ESP32的数字IO,列引脚也接数字IO。本例接线如下(可根据实际调整):

键盘引脚
ESP32引脚
R1IO15
R2IO13
R3IO14
R4IO2
C1IO26
C2IO25
C3IO12
C4IO4

       3. OLED 显示屏连接(I2C)

OLED
ESP32
VCC3.3V
GNDGND
SCLIO22
SDAIO21

复古与科技的碰撞:基于ESP32和4G模块的简易模拟电话机图2



软件实现


开发环境

  • Arduino IDE (需安装ESP32开发板支持)
  • 库安装:通过库管理器安装 Keypad 和 U8g2。

代码结构
程序主要包含以下部分:
  • 初始化:串口、键盘、OLED、SIM7600模块。
  • 键盘扫描:在 loop() 中轮询按键,处理数字累积、拨号、挂断。
  • AT指令处理:通过串口2与SIM7600通信,发送指令并解析响应。
  • 状态机:用 callStatus 变量记录当前状态(IDLE, DIALING, RINGING, ACTIVE),并据此更新屏幕。
  • OLED显示:自定义 updateDisplay() 函数,根据状态显示不同内容。

关键代码片段初始化部分
  1. #include <HardwareSerial.h>
  2. #include <Keypad.h>
  3. #include <U8g2lib.h>
  4. HardwareSerial sim7600(2); // UART2
  5. U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
  6. // 键盘配置
  7. const byte ROWS = 4, COLS = 4;
  8. char keys[ROWS][COLS] = {
  9.   {'1','2','3','A'},
  10.   {'4','5','6','B'},
  11.   {'7','8','9','C'},
  12.   {'*','0','#','D'}
  13. };
  14. byte rowPins[ROWS] = {15,13,14,2};
  15. byte colPins[COLS] = {26,25,12,4};
  16. Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
  17. String phoneNumber = "";
  18. String incomingNumber = "";
  19. String callStatus = "IDLE";
  20. void setup() {
  21.   Serial.begin(115200);
  22.   sim7600.begin(115200, SERIAL_8N1, 16, 17);
  23.   u8g2.begin();
  24.   // 初始化SIM7600
  25.   sendCommand("AT", 1000);
  26.   sendCommand("AT+CPIN?", 1000);
  27.   sendCommand("AT+CREG?", 1000);
  28.   sendCommand("AT+CSQ", 1000);
  29.   sendCommand("AT+CSDVC=3", 1000); // 喇叭输出
  30.   sendCommand("AT+CLIP=1", 1000);   // 来电显示
  31.   updateDisplay();
  32. }
复制代码
通话状态检测(处理模块上报)
  1. while (sim7600.available()) {
  2.   String response = sim7600.readString();
  3.   Serial.print("模块上报: ");
  4.   Serial.println(response);
  5.   if (response.indexOf("RING") >= 0) {
  6.     callStatus = "RINGING";
  7.     // 提取来电号码...
  8.     updateDisplay();
  9.   }
  10.   else if (response.indexOf("CONNECT") >= 0) {
  11.     callStatus = "ACTIVE";
  12.     updateDisplay();
  13.   }
  14.   else if (response.indexOf("VOICE CALL: END") >= 0 ||
  15.            response.indexOf("NO CARRIER") >= 0) {
  16.     callStatus = "IDLE";
  17.     phoneNumber = "";
  18.     incomingNumber = "";
  19.     updateDisplay();
  20.   }
  21. }
复制代码
OLED显示函数(英文版)
  1. void updateDisplay() {
  2.   u8g2.firstPage();
  3.   do {
  4.     u8g2.setFont(u8g2_font_6x10_tf);
  5.     u8g2.setFontPosTop();
  6.     // 状态行
  7.     String statusLine = "Status: ";
  8.     if (callStatus == "IDLE") statusLine += "IDLE";
  9.     else if (callStatus == "DIALING") statusLine += "DIALING...";
  10.     else if (callStatus == "RINGING") statusLine += "INCOMING!";
  11.     else if (callStatus == "ACTIVE") statusLine += "ACTIVE";
  12.     u8g2.drawStr(0, 0, statusLine.c_str());
  13.     // 号码行
  14.     u8g2.setCursor(0, 12);
  15.     if (callStatus == "RINGING") {
  16.       u8g2.print("From:");
  17.       u8g2.setCursor(35, 12);
  18.       u8g2.print(incomingNumber.c_str());
  19.     } else if (callStatus == "DIALING" || callStatus == "ACTIVE") {
  20.       u8g2.print("To:");
  21.       u8g2.setCursor(35, 12);
  22.       u8g2.print(phoneNumber.c_str());
  23.     } else {
  24.       u8g2.print("Number:");
  25.       u8g2.setCursor(45, 12);
  26.       u8g2.print(phoneNumber.c_str());
  27.     }
  28.     // 提示行
  29.     u8g2.setCursor(0, 24);
  30.     if (callStatus == "IDLE") {
  31.       u8g2.print("# Dial   * Clear");
  32.     } else if (callStatus == "RINGING") {
  33.       u8g2.print("* Reject");
  34.     } else if (callStatus == "ACTIVE") {
  35.       u8g2.print("* Hang up");
  36.     } else {
  37.       u8g2.print("Please wait...");
  38.     }
  39.   } while (u8g2.nextPage());
  40. }
复制代码
完整代码
  1. #include <HardwareSerial.h>
  2. #include <Keypad.h>// 需要安装Keypad库
  3. #include <Wire.h>
  4. #include <U8g2lib.h> // 需要安装U8g2库
  5. // --- OLED 配置 (使用U8g2库) ---
  6. // 构造函数:硬件I2C,地址默认为0x3C,复位引脚设为无
  7. U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
  8. // --- SIM7600 配置 ---
  9. HardwareSerial sim7600(2);
  10. //const int PWRKEY_PIN = 4; // 如果不需要控制开机,可注释掉
  11. // --- 键盘配置 (与你实际连接一致) ---
  12. const byte ROWS = 4;
  13. const byte COLS = 4;
  14. char keys[ROWS][COLS] = {
  15.   {'1','2','3','A'},
  16.   {'4','5','6','B'},
  17.   {'7','8','9','C'},
  18.   {'*','0','#','D'}
  19. };
  20. // **请务必根据你的实际接线修改引脚号!**
  21. byte rowPins[ROWS] = {15, 13, 14, 2};
  22. byte colPins[COLS] = {26, 25, 12, 4};
  23. Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
  24. // --- 全局变量 ---
  25. String phoneNumber = "";      // 存储用户输入的号码
  26. String incomingNumber = "";   // 存储来电号码
  27. bool isCallActive = false;    // 通话状态标志
  28. String callStatus = "IDLE";   // 状态: IDLE, DIALING, ACTIVE, RINGING
  29. void setup() {
  30.   Serial.begin(115200);
  31.   delay(1000);
  32.   Serial.println("ESP32 + 键盘拨号 + OLED 示例启动...");
  33.   // --- 初始化OLED ---
  34.   Wire.begin(); // 初始化I2C
  35.   u8g2.begin();
  36.   u8g2.enableUTF8Print(); // 启用UTF8支持,便于显示特殊符号
  37.   updateDisplay();        // 立即更新一次屏幕显示
  38.   // --- 初始化SIM7600 ---
  39.   sim7600.begin(115200, SERIAL_8N1, 16, 17);
  40.   // 如果模块需要PWRKEY引脚触发开机,取消下面的注释
  41.   // pinMode(PWRKEY_PIN, OUTPUT);
  42.   // digitalWrite(PWRKEY_PIN, LOW); delay(1000); digitalWrite(PWRKEY_PIN, HIGH); delay(3000);
  43.   // 发送基础AT指令
  44.   sendCommand("AT", 1000);
  45.   sendCommand("AT+CPIN?", 1000);
  46.   sendCommand("AT+CREG?", 1000);
  47.   sendCommand("AT+CSQ", 1000);
  48.   // 配置音频为喇叭
  49.   sendCommand("AT+CSDVC=3", 1000);
  50.   sendCommand("AT+CLVL=2", 1000);
  51.   // 开启来电显示
  52.   sendCommand("AT+CLIP=1", 1000);
  53.   Serial.println("初始化完成。请使用键盘...");
  54.   callStatus = "IDLE";
  55.   updateDisplay();
  56. }
  57. void loop() {
  58.   // --- 1. 处理键盘输入 (与之前相同) ---
  59.   char key = keypad.getKey();
  60.   if (key) {
  61.     Serial.print("按键: ");
  62.     Serial.println(key);
  63.     if ((key >= '0' && key <= '9') || key == 'A' || key == 'B' || key == 'C' || key == 'D') {
  64.       if (callStatus == "IDLE") { // 仅在待机时允许输入
  65.         phoneNumber += key;
  66.         Serial.print("当前号码: ");
  67.         Serial.println(phoneNumber);
  68.         updateDisplay(); // **每次输入后更新屏幕**
  69.       } else {
  70.         Serial.println("通话中,不能输入");
  71.       }
  72.     }
  73.     else if (key == '#') {
  74.       if (callStatus == "IDLE") {
  75.         if (phoneNumber.length() > 0) {
  76.           makePhoneCall(phoneNumber);
  77.           callStatus = "DIALING"; // 状态变为拨号中
  78.           updateDisplay();
  79.         } else {
  80.           Serial.println("请输入号码");
  81.         }
  82.       } else {
  83.         Serial.println("通话中,不能拨号");
  84.       }
  85.     }
  86.     else if (key == '*') {
  87.       if (callStatus == "ACTIVE" || callStatus == "DIALING" || callStatus == "RINGING") {
  88.         hangupCall();
  89.         // 状态更新将在模块上报 "NO CARRIER" 时进行
  90.       } else {
  91.         phoneNumber = "";
  92.         incomingNumber = "";
  93.         Serial.println("输入已清除");
  94.         updateDisplay();
  95.       }
  96.     }
  97.   }
  98.   // --- 2. 处理SIM7600主动上报的消息 ---
  99.   while (sim7600.available()) {
  100.   String response = sim7600.readString();
  101.   Serial.print("模块上报: ");
  102.   Serial.println(response);
  103.   // 检测来电
  104.   if (response.indexOf("RING") >= 0) {
  105.     callStatus = "RINGING";
  106.     // 提取来电号码...
  107.     int start = response.indexOf('"') + 1;
  108.     int end = response.indexOf('"', start);
  109.     if (start > 0 && end > start) {
  110.       incomingNumber = response.substring(start, end);
  111.     } else {
  112.       incomingNumber = "Unknown";
  113.     }
  114.     updateDisplay();
  115.   }
  116.   // 检测通话接通
  117.   else if (response.indexOf("CONNECT") >= 0) {
  118.     callStatus = "ACTIVE";
  119.     updateDisplay();
  120.   }
  121.   // 检测通话结束(新增对 VOICE CALL: END 的判断)
  122.   else if (response.indexOf("NO CARRIER") >= 0 ||
  123.            response.indexOf("BUSY") >= 0 ||
  124.            response.indexOf("NO ANSWER") >= 0 ||
  125.            response.indexOf("VOICE CALL: END") >= 0) {   // ← 关键添加
  126.     callStatus = "IDLE";
  127.     phoneNumber = "";
  128.     incomingNumber = "";
  129.     updateDisplay();
  130.     Serial.println("通话结束,屏幕已复位");
  131.   }
  132. }
  133. }
  134. // --- 辅助函数 ---
  135. void sendCommand(String cmd, int delayTime) {
  136.   sim7600.println(cmd);
  137.   delay(delayTime);
  138.   while (sim7600.available()) {
  139.     String response = sim7600.readString();
  140.     Serial.println(response);
  141.   }
  142. }
  143. void makePhoneCall(String number) {
  144.   Serial.print("正在拨号: ");
  145.   Serial.println(number);
  146.   String command = "ATD" + number + ";";
  147.   sim7600.println(command);
  148.   delay(3000);
  149.   // 状态由模块上报的 CONNECT/NO CARRIER 更新
  150. }
  151. void hangupCall() {
  152.   Serial.println("挂断电话...");
  153.   sim7600.println("AT+CHUP");
  154.   delay(1000);
  155.   // 状态由模块上报的 NO CARRIER 更新
  156.   // 主动重置状态和屏幕
  157.   callStatus = "IDLE";
  158.   phoneNumber = "";
  159.   incomingNumber = "";
  160.   updateDisplay();
  161. }
  162. // --- OLED 显示函数 ---
  163. void updateDisplay() {
  164.   u8g2.firstPage();
  165.   do {
  166.     // 使用英文字体(简单、省内存)
  167.     u8g2.setFont(u8g2_font_6x10_tf);
  168.     u8g2.setFontPosTop();
  169.     // 第一行:状态 (英文)
  170.     String statusLine = "Status: ";
  171.     if (callStatus == "IDLE") statusLine += "IDLE";
  172.     else if (callStatus == "DIALING") statusLine += "DIALING...";
  173.     else if (callStatus == "RINGING") statusLine += "INCOMING!";
  174.     else if (callStatus == "ACTIVE") statusLine += "ACTIVE";
  175.     else statusLine += callStatus;
  176.     u8g2.drawStr(0, 0, statusLine.c_str());
  177.     // 第二行:号码信息 (英文标签)
  178.     u8g2.setCursor(0, 12);
  179.     if (callStatus == "RINGING") {
  180.       u8g2.print("From:");
  181.       u8g2.setCursor(35, 12);
  182.       u8g2.print(incomingNumber.c_str());
  183.     } else if (callStatus == "DIALING" || callStatus == "ACTIVE") {
  184.       u8g2.print("To:");
  185.       u8g2.setCursor(35, 12);
  186.       u8g2.print(phoneNumber.c_str());
  187.     } else { // IDLE
  188.       u8g2.print("Number:");
  189.       u8g2.setCursor(45, 12); // 调整光标位置以对齐
  190.       u8g2.print(phoneNumber.c_str());
  191.     }
  192.     // 第三行:提示 (英文)
  193.     u8g2.setCursor(0, 24);
  194.     if (callStatus == "IDLE") {
  195.       u8g2.print("# Dial   * Clear");
  196.     } else if (callStatus == "RINGING") {
  197.       u8g2.print("* Reject");
  198.     } else if (callStatus == "ACTIVE") {
  199.       u8g2.print("* Hang up");
  200.     } else {
  201.       u8g2.print("Please wait...");
  202.     }
  203.   } while ( u8g2.nextPage() );
  204. }
复制代码


制作过程与问题解决
       1. 电源是关键
       SIM7600模块在通话时电流可达2A,起初尝试从ESP32的5V引脚取电,导致模块频繁重启。必须使用独立的外部5V电源,并共地。
       2. 通话结束检测
       挂断电话后,模块返回的是 VOICE CALL: END: 000013 而非标准的 NO CARRIER。通过增加对 "VOICE CALL: END" 的检测,成功让屏幕复位。
       3. 汉字乱码
       原计划用中文显示状态,但普通英文字体不支持中文。最终改用全英文显示,避免了字库加载的麻烦,也节省了内存。
       4. 键盘响应
       4×4键盘的行列引脚必须正确映射,否则按键会错乱。可根据键盘背面的内部连线图调整 rowPins 和 colPins 的顺序。
复古与科技的碰撞:基于ESP32和4G模块的简易模拟电话机图3
复古与科技的碰撞:基于ESP32和4G模块的简易模拟电话机图4
最终效果
  • 待机状态:屏幕显示“Status: IDLE”,下方提示“# Dial * Clear”。
  • 输入号码:按下数字键,号码实时显示在“Number:”后面。
  • 拨号:按“#”后状态变为“DIALING...”,等待接通。
  • 通话中:状态变为“ACTIVE”,显示对方号码。
  • 来电:状态变为“INCOMING!”,显示来电号码,按“*”拒接。
  • 挂断:通话结束,屏幕自动恢复待机状态。

演示视频

总结与展望
       这个项目将复古的按键操作与现代4G通信技术结合,打造了一个简单有趣的通信终端。通过亲手搭建,我不仅熟悉了ESP32的编程、AT指令集的使用,还学会了处理硬件供电、串口通信等实际问题。未来,我计划将它应用到智能拐杖项目中,实现老人跌倒后,自动拨打电话求助,并通过短信功能发送位置信息(本项目使用的4G模块支持GNSS卫星定位(GPS、CLONASS、BD))。
       如果你也想制作一个这样的电话机,欢迎参考我的代码和接线,一起享受创造的乐趣!




您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail