用一块带 4 寸触摸屏的开发板 + 一片 4G Cat.1 模组,从零实现一台能打电话的电话机: 触屏拨号盘、来电弹窗显号、通话计时、免提通话、大音量振铃——全部开源方案。
<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=117444773484302&bvid=BV1kMJy65Eti&cid=42767092319&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>

一、这是什么项目
用 STM32H747I-DISCO(Cortex-M7 @ 400MHz,4" 800x480 MIPI-DSI 电容触摸屏)
UniKnect Gen1-PRO 扩展板(移远 EC200U 4G Cat.1 模组), 在 Zephyr RTOS + LVGL 9.5 上实现了一台功能完整的触屏电话:
触屏数字键盘拨号、拨打/挂断
来电弹窗显示号码、接听/拒接、大音量振铃
通话中计时、免提喇叭双向通话
4G 网络状态实时显示(注册状态/信号)
软件零闭源依赖:Zephyr(Apache-2.0)+ LVGL(MIT)+ uniknect-c(移远官方 C 框架,Apache-2.0)。
二、硬件连接
UniKnect 扩展板提供 EC200U 模组及其电源管理。与 DISCO 之间只需 3 根线:
UniKnect 侧(J308 跳线块)信号DISCO 侧
| pin 3(MCU_TXD,模组收) | UART TX | D1 / PJ8(UART8_TX) |
| pin 4(MCU_RXD,模组发) | UART RX | D0 / PJ9(UART8_RX) |
| J305 pin 5(GND) | 地 | Arduino GND |
J308 针脚编号:左列从上到下 1/3/5,右列 2/4/6。拔掉 J308 上原有跳线帽再飞线。 两板各自独立供电(UniKnect 插自己的 DC5V,DISCO 插自己的 USB),只共享信号和地。

音频:
通话声音:EC200U 的 SPK 输出 → UniKnect 板上的喇叭
振铃:DISCO 板载 WM8994 codec → CN11 绿色耳机口/喇叭(比模组自带铃声大得多)
三、软件架构
┌─────────────────────────────────────────────┐
│ phone_ui.c LVGL 9.5 界面(拨号盘/来电/通话) │ ← 主线程:触摸 + 显示
├─────────────────────────────────────────────┤
│ modem_task.c 电话状态机(独立线程) │ ← AT 命令全在这
├─────────────────────────────────────────────┤
│ ringtone.c 铃声生成(正弦波→SAI/I2S→WM8994)│ ← 独立线程
├─────────────────────────────────────────────┤
│ at_client(uniknect-c,RT-Thread 血统 AT 引擎)│
│ OSAL(CMSIS-RTOS2 风格接口 → Zephyr 移植层) │
│ hal_uart(UART8 中断收发 + 环形缓冲) │
├─────────────────────────────────────────────┤
│ Zephyr v4.4 内核 + 驱动(UART/DSI/LTDC/I2C…) │
└─────────────────────────────────────────────┘
线程模型:UI 主循环(10ms tick)/ 模组任务(AT 独占)/ 铃声线程(DMA 喂块), 三者经 k_msgq + 共享状态通信,互不阻塞。
四、关键实现
4.1 电话状态机(modem_task.c)
EC200U 的语音通话就是几条 AT 命令的事,配上 URC 监听就是完整状态机:
/* 状态机:IDLE / DIALING / INCOMING / ACTIVE */
switch (state) {
case IDLE:
/* RING URC → INCOMING(+CLIP 抓来电号码) */
break;
case INCOMING:
/* UI 弹窗;ATA 接听 → ACTIVE;ATH 拒接 → IDLE */
break;
case DIALING:
/* ATD<号码>; 已发;每秒 AT+CLCC 轮询,stat=0 即接通 → ACTIVE */
break;
case ACTIVE:
/* 通话计时;ATH 或 NO CARRIER → IDLE */
break;
}
来电显号和铃声使能(初始化时一次设置):
at_exec(client, "AT+CLIP=1"); /* 来电显示号码 */
at_exec(client, "AT+QAUDMOD=2"); /* 音频通道:免提喇叭 */
at_exec(client, "AT+CLVL=5"); /* 音量最大 */
URC 注册(at_client 的 URC 表机制,来电/挂断异步通知):
static const at_urc_t phone_urc[] = {
{"RING", "\r\n", on_ring, NULL}, /* 来电 */
{"+CLIP:", "\r\n", on_clip, NULL}, /* 来电号码 */
{"NO CARRIER", "\r\n", on_no_carrier, NULL}, /* 对方挂断 */
};
at_obj_set_urc_table(client, phone_urc, ARRAY_SIZE(phone_urc));
4.2 LVGL 拨号界面(phone_ui.c)
800x480 大屏,手写 LVGL(不依赖 GUI Guider)。数字键盘是 3x4 按钮矩阵:
static const char *keys[] = {"1","2","3","4","5","6","7","8","9","*","0","#"};
for (int i = 0; i < 12; i++) {
lv_obj_t *btn = lv_button_create(keypad);
lv_obj_set_size(btn, 180, 70); /* 大按钮好按 */
lv_obj_set_pos(btn, 40 + (i % 3) * 210, 130 + (i / 3) * 82);
lv_obj_add_event_cb(btn, on_digit, LV_EVENT_PRESSED, (void *)keys[i]);
/* 用 PRESSED 而非 CLICKED:手指轻微移动时 CLICKED 会被判成拖拽丢弃 */
lv_obj_t *lb = lv_label_create(btn);
lv_label_set_text(lb, keys[i]);
lv_obj_set_style_text_font(lb, &lv_font_montserrat_24, 0);
lv_obj_center(lb);
}
三个界面用 lv_screen_load() 切换:拨号屏 / 来电屏(大号号码 + 接听/挂断)/ 通话屏(计时 + 挂断)。

4.3 大音量振铃(ringtone.c)
模组自带铃声音量太小(内置 PA 上限),改用 DISCO 板载 WM8994 codec 播放程序生成的铃声—— 不需要音频文件,正弦波查表生成:
/* 双音 440+480Hz(美式回铃音),44.1kHz/16bit/立体声 */
#define TONE_AMP 12000
static void fill_block(int16_t *buf, size_t frames)
{
static uint32_t phase1, phase2;
for (size_t i = 0; i < frames; i++) {
int32_t s = (sine(phase1) + sine(phase2)) / 2; /* 双音叠加 */
buf[i*2] = buf[i*2+1] = (int16_t)(s * TONE_AMP / 32767);
phase1 += PHASE_STEP_440HZ; phase2 += PHASE_STEP_480HZ;
}
}
/* 节奏:响 1s 停 2s,来电期间循环;接听/挂断即停 */
音频通路(SAI1 + I2S DMA → WM8994 → CN11 耳机口),初始化时序要注意 "先起 SAI 时钟再配 codec"(codec 的电荷泵需要 MCLK/BCLK 在场)。
五、编译与烧录
环境:Windows + Zephyr 4.4(工作区自带 SDK 1.0.1),Git Bash:
export ZEPHYR_BASE=/d/luglZephyrproject/zephyr
export ZEPHYR_SDK_INSTALL_DIR=/d/luglZephyrproject/zephyr-sdk-1.0.1/zephyr-sdk-1.0.1
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
cd /d/luglZephyrproject
west build -b stm32h747i_disco/stm32h747xx/m7 \
--shield st_b_lcd40_dsi1_mb1166_a09 -p always -d build/h747phone \
/d/luglZephyrproject/app/stm32H747_phone
west flash -d build/h747phone
控制台(ST-Link VCP,115200)启动日志:
*** Booting Zephyr OS build v4.4.0-25 ***
<inf> ringtone: audio hw ready
[INFO] [modem] probing AT... (round 0)
[INFO] [modem] online (round 0)
[modem] registered, phone ready

六、成果
功能清单
功能实现
| 触屏拨号 | LVGL 数字键盘,ATD 拨出 |
| 通话 | 免提双向通话(喇叭+模组 mic),MM:SS 计时 |
| 来电 | RING 弹窗 + 号码显示 + 大音量振铃(DISCO 板载音频) |
| 挂断 | 主动挂断(ATH)/ 对方挂断(NO CARRIER)都识别 |
| 网络 | 4G 自动注册,状态栏实时显示 |
资源占用
FlashRAM
| ~355KB / 1MB(35%) | ~168KB / 512KB(33%) |
(H747 还有 M4 核和 8MB SDRAM 完全没用上——扩展空间很大)
整机实拍

七、技术要点回顾
电话 = 几条 AT 命令:ATD(拨)/ ATA(接)/ ATH(挂)/ +CLIP(显号)/ AT+CLCC(状态轮询)—— 4G 模组的语音功能没想象中复杂
分层抽象是移植的关键:OSAL + hal_uart 两层换掉,整个 AT 框架(at_client/ql_*)零修改跨平台
大屏触摸 + LVGL 是绝配:800x480 的拨号键盘比实体按键手感好
音频可以"借":模组铃声小就换板载 codec——程序生成正弦波,连音频文件都不用
Uniknect 板的正确用法是飞线不是叠插:3 根线(TX/RX/GND)比整个 Arduino 排针可靠得多
我要赚赏金
