01 接打电话(Volte)
作者:陈媛媛 | 最后修改:2026-05-09
一、概述
VoLTE(Voice over LTE)是通过 LTE 网络实现的高清语音通话技术,基于 IMS(IP 多媒体子系统)架构,支持语音与数据并发传输,提供更低时延、更高清晰度的通话体验。
本章节将使用 Air780EHV 核心板 +AirAUDIO_1000 音频扩展板演示 VOLTE 通话功能的使用方法。
二、演示功能概述
-
音频设备初始化与管理
-
统一初始化所有硬件 GPIO 引脚和复用引脚(I2C0_SCL/I2C0_SDA);
- 配置音频芯片 ES8311 的电源控制序列:电源控制 -> I2C 配置 -> I2S 配置 -> 音频通道配置 -> 音量设置;
-
支持喇叭功放 PA 控制,提供完整的电源时序管理;
-
完整的 VoLTE 通话业务逻辑
提供 4 种通话场景(通过设置 ACTIVE_SCENARIO 变量选择):
- 场景 1:呼入立即挂断(响铃 3 次后自动拒接);
- 场景 2:呼入自动接听(响 2 声)+ 10 秒后主动挂断;
- 场景 3:呼入自动接听(响 2 声)+ 等待对方挂断;
-
场景 4:主动呼出电话 + 等待对方挂断;
-
通话状态监控与处理:
-
订阅“CC_IND”事件,实时处理来电、接听、通话建立、挂断等状态;
-
每个场景都有独立的状态机逻辑和计数器管理;
-
通话录音功能:
-
创建 4 个 zbuff 缓冲区用于保存上下行音频数据(up1/up2/down1/down2);
- 实现录音回调,实时记录通话质量;
-
支持上行和下行音频数据的分别存储;
-
音频电源管理:
-
在通话开始时自动唤醒音频设备;
-
在通话结束后设置音频芯片进入待机模式,降低功耗;
-
硬件平台支持:
-
支持 Air780EHV 核心板 +AirAUDIO_1000 音频扩展板;
- 自动适配不同的引脚定义和电源控制逻辑;
注意事项
- 使用前必须设置 ACTIVE_SCENARIO 变量选择需要的场景(1-4);
- 场景 4 需要修改 outgoing_number 为实际测试号码;
- 所有通话都会被自动录音,需确保设备有足够的存储空间;
- 音频芯片与 GSensor/触摸屏共用 I2C0 总线,使用时需注意功能冲突;
三、准备硬件环境
参考:硬件环境清单第二章节内容,准备以及组装好硬件环境。
3.1 Air780EHV 核心板 +AirAUDIO_1000 配件板 + 喇叭
Air780EHV 核心板购买链接:点击购买 air780EHV 核心板 +AirAUDIO_1000 配件版
Air780EHV 核心板 +AirAUDIO_1000 音频扩展板接线图

Air780EHV 核心板和 AirAUDIO_1000 配件板的硬件接线方式为:
| Air780EHV核心板 | AirAUDIO_1000配件板 |
| 3/MIC+ | MIC+ |
| 4/MIC- | MIC- |
| 5/SPK+ | SPK+ |
| 6/SPK- | SPK- |
| 19/GPIO22 | PA_EN |
| VBAT | VCC |
| GND | GND |
3.2 具备 volte 功能的电话卡插入开发板/核心板的 sim 卡槽
3.3 TYPE-C USB 数据线一根
- Air780EHV 核心板通过 TYPE-C USB 口供电;
- TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
四、准备软件环境
- 烧录工具 Luatools;
- 内核固件:本demo开发测试时使用的固件为Air780EHV V2016 版本固件(请选择支持 Volte 功能固件),所以你如果要测试本demo时,可以直接使用最新版本支持 Volte 功能的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;
- LuatOS 需要的脚本和资源文件:点我浏览所有文件
- lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件;
- 合宙 LuatIO 工具(GPIO 复用初始化配置)使用说明 https://docs.openluat.com/air780epm/common/luatio/
准备好软件环境之后,接下来查看如何烧录项目文件到 Air780EHV 核心板中。
五、软硬件资料
5.1 API 接口介绍
-
exaudio - 音频扩展库 https://docs.openluat.com/osapi/ext/exaudio/
exaudio 是 audio 的扩展库,简化了使用方法,扩展了部分应用,建议使用此库。
- CC_IND -- 通话状态变化
"READY":通话准备完成,可以拨打电话或者呼入电话了
"INCOMINGCALL":有电话呼入
"CONNECTED":电话已经接通
"DISCONNECTED":电话被对方挂断
"SPEECH_START":通话开始
"MAKE_CALL_OK":拨打电话请求成功
"MAKE_CALL_FAILED":拨打电话请求失败
"ANSWER_CALL_DONE":接听电话请求完成
"HANGUP_CALL_DONE":挂断电话请求完成
"PLAY":开始有音频输出
5.2 硬件设计介绍
要满足 VOLTE 功能的话需要选用带有录音功能的 codec 芯片,780EHV 模块内部是 es8311,电路参考设计可参考音频电路参考设计,使用时通过 gpio 控制使能引脚。

六、代码解析
6.1 主程序 (main.lua)
语音通话应用主入口,负责加载功能模块
--[[
@module main
@summary LuatOS语音通话应用主入口,负责加载功能模块
@version 1.0
@date 2025.07.16
@author 陈媛媛
@usage
本demo演示的核心功能为:
1、音频设备初始化与控制
2、完整通话业务逻辑处理(4种通话场景),详情如下:
-呼入,挂断,挂断消息识别打印;
-呼入,接听,接听消息识别打印,录音,主动挂断,挂断消息识别打印;
-呼入,接听,接听消息识别打印,录音,对方挂断,挂断消息识别打印;
-呼出,对方接通,接听消息识别打印,建立通话后一段时间,对方主动挂断,挂断消息识别打印;
3、通话状态监控与日志记录
4、支持Air8000系列和Air780EHV等硬件平台
更多说明参考本目录下的readme.md文件
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "VOICE_CALL_DEMO"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
--配置喂狗超时时间为9秒钟
wdt.init(9000)
--启动一个循环定时器,每隔3秒钟喂一次狗
sys.timerLoopStart(wdt.feed, 3000)
end
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 仅加载必要的功能模块
require "audio_drv" -- 音频设备管理模块
require "cc_app" -- 通话业务逻辑模块
-- 用户代码已结束---------------------------------------------
sys.run()
-- sys.run()之后不要加任何语句!!!!!
6.2 音频设备管理模块(audio_drv.lua)
本模块提供以下功能:
1、定义所有硬件引脚常量
2、使用 exaudio 扩展库初始化音频设备
--[[
@module audio_drv
@summary 音频设备管理模块,负责音频设备的初始化和控制(仅使用exaudio扩展库)
@version 2.0
@date 2025.10.23
@author 陈媛媛
@usage
本模块提供以下功能:
1、定义所有硬件引脚常量
2、使用exaudio扩展库初始化音频设备
]]
-- 引入exaudio库
local exaudio = require("exaudio")
-- exaudio配置参数
local audio_configs = {
model = "es8311", -- dac类型: "es8311"
i2c_id = 0, -- i2c_id: 可填入0,1 并使用pins 工具配置对应的管脚
pa_ctrl = gpio.AUDIOPA_EN, -- 音频放大器电源控制管脚
dac_ctrl = 20, -- 音频编解码芯片电源控制管脚
dac_delay = 3, -- DAC启动前冗余时间(单位100ms)
pa_delay = 100, -- DAC启动后延迟打开PA的时间(单位1ms)
dac_time_delay = 100, -- 播放完毕后PA与DAC关闭间隔(单位1ms)
bits_per_sample = 16, -- 采样位深
pa_on_level = 1 -- PA打开电平 1:高 0:低
}
exaudio.vol(70) -- 喇叭音量
exaudio.mic_vol(65) -- 麦克风音量
-- 初始化音频设备
local function initAudioDevice()
-- 使用exaudio.setup统一配置音频设备
log.info("audio_drv", "使用exaudio.setup初始化音频设备")
if exaudio.setup(audio_configs) then
log.info("audio_drv", "exaudio.setup初始化成功")
else
log.error("audio_drv", "exaudio.setup初始化失败")
return false
end
-- log.info("audio_drv", "Audio device initialized using exaudio only")
return true
end
-- 获取音频通道ID(保留用于兼容性)
local function getMultimediaId()
return 0 -- 返回默认值0
end
-- 导出接口
return {
initAudioDevice = initAudioDevice,
getMultimediaId = getMultimediaId
}
6.3 通话业务逻辑模块(cc_app.lua)
本模块提供以下功能:
- 设置 ACTIVE_SCENARIO 变量选择需要的场景(1-4)
- 其他场景自动禁用,不会影响程序运行
- 每个场景都有独立的状态处理逻辑
- 实现通话录音功能
支持的语音通话场景:
[场景 1] 呼入立即挂断(响铃 3 次后自动拒接)
[场景 2] 呼入自动接听(响 2 声)+10 秒后主动挂断
[场景 3] 呼入自动接听(响 2 声)+ 等待对方挂断
[场景 4] 主动呼出电话 + 等待对方挂断
--[[
@module cc_app
@summary 通话业务逻辑模块,实现4种通话场景的处理和通话录音功能
@version 1.0
@date 2025.10.21
@author 陈媛媛
@usage
本模块提供以下功能:
1. 设置 ACTIVE_SCENARIO 变量选择需要的场景(1-4)
2. 其他场景自动禁用,不会影响程序运行
3. 每个场景都有独立的状态处理逻辑
4. 实现通话录音功能
支持的场景:
[场景1] 呼入立即挂断(响铃3次后自动拒接)
[场景2] 呼入自动接听(响2声)+10秒后主动挂断
[场景3] 呼入自动接听(响2声)+等待对方挂断
[场景4] 主动呼出电话+等待对方挂断
注意事项:
1. 设置ACTIVE_SCENARIO选择要启用的场景(1-4)
2. 场景4会主动拨打电话(修改为自己测试时要拨打的电话号码)
3. 所有通话都会被录音(需确保存储空间足够)
]]
-- 引入音频设备模块
local audio_drv = require "audio_drv"
-- ====================== 配置区域 ======================
-- 设置当前激活的场景(1-4),注释掉不需要的场景
-- local ACTIVE_SCENARIO = 1 -- 场景1:呼入立即挂断
-- local ACTIVE_SCENARIO = 2 -- 场景2:呼入自动接听,10秒后主动挂断
--local ACTIVE_SCENARIO = 3 -- 场景3:呼入自动接听,等待对方挂断
-- local ACTIVE_SCENARIO = 4 -- 场景4:主动呼出,等待对方挂断
-- 全局状态变量
local call_counter = 0 -- 响铃计数器(用于场景1-3)
local caller_number = "" -- 来电号码(用于场景1-3)
local is_connected = false -- 通话连接状态标志(用于场景2)
local outgoing_number = "10000" -- 呼出号码(用于场景4),修改为自己测试时要拨打的电话号码
-- ====================== 录音功能 ======================
-- 创建音频数据缓冲区
local up1 = zbuff.create(6400,0) -- 上行数据保存区1
local up2 = zbuff.create(6400,0) -- 上行数据保存区2
local down1 = zbuff.create(6400,0) -- 下行数据保存区1
local down2 = zbuff.create(6400,0) -- 下行数据保存区2
-- 音频数据回调函数
local function recordCallback(is_dl, point)
if is_dl then
log.info("录音", "下行数据,位于缓存", point+1, "缓存1数据量", down1:used(), "缓存2数据量", down2:used())
else
log.info("录音", "上行数据,位于缓存", point+1, "缓存1数据量", up1:used(), "缓存2数据量", up2:used())
end
log.info("通话质量", cc.quality())
-- 可以在初始化串口后,通过uart.tx来发送走对应的zbuff即可
end
-- 启用通话录音
local function enableRecording()
cc.record(true, up1, up2, down1, down2)
cc.on("record", recordCallback)
log.info("cc_app", "通话录音已启用")
end
-- 获取所有缓冲区
local function getRecordingBuffers()
return {
up1 = up1,
up2 = up2,
down1 = down1,
down2 = down2
}
end
-- ====================== 场景处理函数 ======================
-- 场景1:呼入立即挂断(响铃3次后)
local function handle_scenario1(status)
if status == "INCOMINGCALL" then
-- 获取来电号码
caller_number = cc.lastNum() or "未知号码"
call_counter = call_counter + 1
log.info("场景1", "收到来电,号码:", caller_number, "响铃次数:", call_counter)
-- 响铃3声后拒接
if call_counter >= 3 then
log.info("场景1", "拒接来电")
cc.hangUp(0)
call_counter = 0 -- 重置计数器
end
elseif status == "HANGUP_CALL_DONE" then
log.info("场景1", "挂断完成")
call_counter = 0
end
end
-- 场景2挂断回调函数
local function scenario2_hangup_callback()
log.info("场景2", "10秒通话结束,主动挂断")
cc.hangUp(0)
is_connected = false
end
-- 场景2:呼入自动接听,10秒后主动挂断
local function handle_scenario2(status)
if status == "INCOMINGCALL" then
-- 获取来电号码
caller_number = cc.lastNum() or "未知号码"
call_counter = call_counter + 1
log.info("场景2", "收到来电,号码:", caller_number, "响铃次数:", call_counter)
-- 响铃2声后自动接听
if call_counter >= 2 then
log.info("场景2", "自动接听来电")
cc.accept(0)
call_counter = 0 -- 重置计数器
end
elseif status == "ANSWER_CALL_DONE" then
log.info("场景2", "接听完成,等待通话建立")
elseif status == "SPEECH_START" then
-- 语音通话真正开始
if not is_connected then
log.info("场景2", "通话已建立,开始计时")
is_connected = true
-- 创建10秒后挂断的定时器
sys.timerStart(scenario2_hangup_callback, 10000) -- 10秒后执行挂断
log.info("场景2", "10秒挂断定时器创建成功")
end
elseif status == "HANGUP_CALL_DONE" or status == "DISCONNECTED" then
log.info("场景2", "通话结束")
is_connected = false
-- 取消挂断定时器
sys.timerStop(scenario2_hangup_callback)
log.info("场景2", "已取消挂断定时器")
call_counter = 0 -- 重置计数器
end
end
-- 场景3:呼入自动接听,等待对方挂断
local function handle_scenario3(status)
if status == "INCOMINGCALL" then
-- 获取来电号码
caller_number = cc.lastNum() or "未知号码"
call_counter = call_counter + 1
log.info("场景3", "收到来电,号码:", caller_number, "响铃次数:", call_counter)
-- 响铃2声后自动接听
if call_counter >= 2 then
log.info("场景3", "自动接听来电")
cc.accept(0)
call_counter = 0 -- 重置计数器
end
elseif status == "SPEECH_START" then
-- 语音通话真正开始
log.info("场景3", "电话已接通,电话号码:", caller_number)
elseif status == "DISCONNECTED" then
-- 对方挂断通话
log.info("场景3", "通话结束对方挂断")
call_counter = 0 -- 重置计数器
end
end
-- 场景4:主动呼出,等待对方挂断
local function handle_scenario4(status)
if status == "CONNECTED" then
log.info("场景4", "呼叫接通")
elseif status == "DISCONNECTED" then
log.info("场景4", "通话结束(对方挂断)")
elseif status == "MAKE_CALL_FAILED" then
log.info("场景4", "呼叫失败")
end
end
-- 场景4拨号函数
local function dial_for_scenario4()
log.info("场景4", "开始拨打", outgoing_number)
cc.dial(0, outgoing_number)
end
-- ====================== 主事件处理器 ======================
sys.subscribe("CC_IND", function(status)
log.info("CC状态", status)
-- 根据激活的场景调用对应的处理函数
if ACTIVE_SCENARIO == 1 then
handle_scenario1(status)
elseif ACTIVE_SCENARIO == 2 then
handle_scenario2(status)
elseif ACTIVE_SCENARIO == 3 then
handle_scenario3(status)
elseif ACTIVE_SCENARIO == 4 then
handle_scenario4(status)
end
-- 所有场景都需要处理的通用状态
if status == "READY" then
sys.publish("CC_READY") -- 发布系统就绪事件
-- 场景4:电话系统就绪后自动拨号
if ACTIVE_SCENARIO == 4 then
sys.timerStart(dial_for_scenario4, 1000) -- 延迟1秒拨号
end
elseif status == "HANGUP_CALL_DONE" or status == "MAKE_CALL_FAILED" or status == "DISCONNECTED" then
audio.pm(0,audio.STANDBY)
-- audio.pm(0,audio.SHUTDOWN) --低功耗可以选择SHUTDOWN或者POWEROFF,如果codec无法断电用SHUTDOWN
end
end)
-- ====================== 电话系统初始化 ======================
local function init_cc()
-- 初始化音频设备(使用exaudio)
audio_drv.initAudioDevice()
-- 等待电话系统就绪
sys.waitUntil("CC_READY")
-- 初始化电话功能
cc.init(audio_drv.getMultimediaId())
-- 启用通话录音(录音功能在cc_app中)
enableRecording()
log.info("cc_app", "电话系统初始化完成")
end
-- 启动初始化任务
sys.taskInit(init_cc)
log.info("cc_app", "通话业务逻辑模块加载完成,当前场景:", ACTIVE_SCENARIO)
-- 导出录音相关功能(如果需要被其他模块使用)
-- return {
-- enableRecording = enableRecording,
-- getRecordingBuffers = getRecordingBuffers,
-- recordCallback = recordCallback
-- }
七、运行结果展示
场景 1:呼入立即挂断(响铃 3 次后自动拒接)

场景 2:呼入自动接听(响 2 声)+10 秒后主动挂断

场景 3: 呼入自动接听(响 2 声)+ 等待对方挂断

场景 4:主动呼出电话 + 等待对方挂断

七、总结
至此,我们已经用 Air780EHV 核心板 +AirAUDIO_1000 配件板 音频扩展板 演示了 VOLTE 功能。
八、常见问题与注意事项
打不了电话
- 要确认固件是否支持 VOLTE 功能,只有支持 VOLTE 功能的固件才能进行通话
- 确认 sim 卡是否开通 VOLTE 功能,sim 卡有没有欠费。