做嵌入式音视频设备的开发者大概率都碰过Hi3536这颗芯片。它在NVR、视频采集盒这类产品里出镜率很高常见的音频需求就是录音和放音。我这次拿到手的项目更直接产品只有一路麦克风输入扬声器也是单声道但客户要求编码出的视频音频轨必须能被播放器识别为双声道这就引出了标题里说的“Hi3536音频单声道改双声道”。这需求听起来不复杂真正做起来却要跨硬件通路、内核驱动、应用编码三层踩过一圈坑之后我想把完整的排查过程和改动方案记录下来给同样在这个芯片平台上做音频适配的朋友一个参考。1. 先弄清楚单声道音频到底“单”在哪一层1.1 硬件单声道与软件单声道的区别动手之前必须先分清所谓“单声道”到底是在哪个环节形成的。有的产品是硬件上只有一路MIC输入音频编解码芯片只做单路模拟信号采样这样采集到的PCM数据天然就是单声道。有的产品硬件其实支持两路输入但驱动代码里只使能了其中一路通道。还有一种是硬件立体声驱动和配置也都是立体声但上层应用创建编码通道时填写音频编码参数结构体时把声道数写成了1最终封装出来的视频文件音轨就是单声道。这三种情况的改法完全不同。硬件单声道最麻烦因为模拟前端只有一路输入你没法简单靠配置就真的变成两路独立信号只能在软件端做数据处理把一份数据复制成左右两份驱动只开了单通道的情况相对好办找到音频输入模块的通道选择寄存器或SDK配置把另一路使能即可纯应用层参数错误则最简单改一个变量就能恢复。绝大多数批量生产的板卡到送测阶段硬件早就定型了你能动的只有驱动和应用层目标也变成了“让系统声卡上报为双声道播放和录制出来的内容左右声道都有有效信号”。这里有个特别容易混淆的概念用户说“我要双声道”有时候只是希望耳机或喇叭两边都有声音有时候则是要求录像文件在后期编辑软件里能看到两个分开的波形轨道。这两种诉求的落地路径完全不同。前者只要在输出侧做数据复制就行后者必须让采集、编码、封装整条链路都以双声道去解析和处理。我这次接到的需求属于后者客户明确要检查音轨的立体声属性所以从一开始就得按完整链路来改。1.2 为什么上层复制了数据播放出来还是单声道很多开发者第一反应是“底层不改我在应用层把单声道PCM复制一份变成双声道不就行了吗”。这个思路在PC上玩过音频处理的人都很容易产生但在Hi3536平台上会遇到一个非常迷惑的问题上层数据明明已经是左声道一份、右声道一份播放出来还是单声道甚至用播放器打开录像文件音轨依旧显示1声道。原因在于Hi3536的音频链路并不是简单把应用写的数据直接送到物理输出。音频输入和输出由芯片内部专门的音频模块管理数据要通过DMA搬运、I2S或数字音频接口传输到外置codec。驱动初始化I2S时会根据配置的声道数来决定帧格式比如LRCK信号的解析方式、每个采样周期搬几个字节。如果驱动里还是单声道配置上层送进来的双声道数据在DMA搬运时会被底层按单声道帧格式拆解。举个例子一个左声道采样点是0x0101右声道采样点是0x0202在单声道配置的底层眼中这会被当成两个独立的单声道采样点来处理数据错位最终听感上依然是一路信号。更隐蔽的是编码器环节。Hi3536支持硬件音频编码创建编码通道时有一个音频编码参数的配置项明确写着声道数。如果这个参数保持默认的1硬件编码器会按单声道的采样节奏去读取输入PCM缓存哪怕前面驱动已经正确处理成了双声道交织数据编码器也只取其中一半来用最终码流里的声道信息自然还是1。所以“单声道改双声道”严格来说是一串连锁改动不是某一个点的调整。1.3 把目标拆解成三个可验证的小结论我在项目一开始就给自己定了一个可验证的验收标准把“改双声道”拆成三个必须同时满足的小目标。第一采集端能获得双声道的PCM数据换句话说应用层从音频输入接口读到的帧缓冲区里左右声道的内容都在而且格式正确。第二输出端能按双声道的帧格式把数据送出去也就是I2S的LRCK翻转正常、DMA的传输字节数与周期匹配、codec侧的左右声道寄存器都配置正确。第三音视频封装层的声道信息必须为2编码器创建通道时的参数、封装模块写入视频文件音轨头的信息都要同步更新。这三个目标每完成一个我就用对应的工具做一次验证避免所有问题攒到最后一起排查。实际做下来这个习惯帮了大忙因为链路里每一层的改动都可能牵动下一层。比如仅仅把AI通道的声道数改成2如果codec硬件只接了单路输入那右声道采到的一直是接近静音的无效值这种“假立体声”在波形图上能看出来但靠耳朵听很可能忽略。先把目标拆细、定好验收方法再动手改代码后面会省掉大量来回试错的时间。2. 改双声道的整体思路与方案取舍2.1 四种可行方案硬件、驱动、应用、伪装我在评估这个改造时整理了四条技术路线按彻底程度从高到低排开。第一条是硬件真改在设备上增加第二路MIC输入或线性输入从模拟前端就实现真正的立体声采集。这是最理想的情况但需要改原理图和PCB量产板卡基本不考虑。第二条是驱动层扩展保持硬件单输入不变在音频驱动内部把采集到的单声道数据复制成双声道同时把I2S和DMA配置改成双声道模式。这条方案的普适性最好所有跑这个驱动的应用都会自动获得“立体声”能力。第三条是应用层处理业务代码里自己复制PCM数据再以双声道格式送入编码器或播放器。这条方案改动局部适合驱动难以改动或时间紧张的场景。第四条是封装伪装只修改视频文件容器里的声道信息字段让播放器误认为这是双声道。第四条基本属于歪门邪道因为播放器在解码时发现实际数据长度和声道数对不上轻则静音重则产生持续爆音。但对于纯演示环境偶尔有人这么干我只能说慎用。对我这次的情况硬件改不了应用层方案虽然快但项目里有多个业务进程都在采集和播放音频每个进程都要处理一遍漏掉一个就出问题所以最后选了驱动层方案作为主线应用层逻辑反而不用动。2.2 为什么优先做驱动层改动从维护角度讲驱动层的改动有一个天然优势用户态程序看到的音频接口参数是统一的改完后所有调用方都能透明获得双声道能力不用逐个进程适配。而且Hi3536的SDK把音频模块的接口封得比较完整改动通常集中在几个关键配置结构体里不涉及大量算法逻辑。相比改应用代码驱动层改动引入的不确定因素反而更可控。但驱动层方案也有明显代价。第一修改后影响范围是全系统的所有音频通路如果板卡同时把同一路I2S引到HDMI或模拟输出声道数变化可能会导致另一端设备解码异常。第二需要重新编译内核模块并烧录每验证一次都要走一遍完整的集成流程。第三DMA相关的buffer大小和周期必须重新计算一旦算错跑一段时间后会出现周期性爆音这类问题最让人头疼。权衡之下我还是坚持驱动层方案因为后续产品线可能复用这套驱动一次投入长期受益。2.3 常见方案的横向对比为了更直观地说明选型依据我把这几条路线整理成了对比表方便遇到类似需求的朋友直接参考。改造层级实现方式改动量风险点适用场景硬件增加第二路MIC、调整模拟前端大高需要重新打样新品定义阶段驱动I2S/DMA/codec配置改为双声道内部做单声道数据扩展中中影响全系统音频通路多数量产板卡升级应用应用内复制PCM并设置编码器声道数小低但每个进程都要处理时间紧、驱动不好动封装伪装只修改容器中的声道信息极小高播放器可能解码异常临时演示不建议表格里的驱动方案风险写的是“中”原因是声道数变化后同一路数字音频接口输出的数据格式整体变了。比如某块板子的I2S音频同时供给本地喇叭和HDMIHDMI对端设备如果只支持单声道PCM收到立体声流后可能直接不输出声音。这种问题在前期评估时很难发现只有在验证阶段覆盖各输出接口的实际场景才能暴露。我自己就在这部分吃过亏后面会专门展开讲。3. 实操过程从驱动到编码器的完整链路修改3.1 准备环境与确认当前音频通路动手前先把环境和验证工具备齐。Hi3536的SDK一般自带内核源码、音频库和示例程序交叉编译工具链要保证能正常编译出内核模块。板卡跑起来之后我习惯先做三件事一是通过调试串口进入系统查看声卡设备注册情况二是用SDK自带的音频采集示例把一段PCM数据保存出来三是确认应用层拿到的PCM格式参数重点是声道数。这三个信息是后面所有改动的基准。这里有一个很实用的小技巧保存PCM数据时不要只存几十个字节至少采集一秒钟的数据也就是在48kHz采样率、16bit位深下单声道大约96KB左右的数据量。拿到数据后放到PC上用音频分析工具打开看波形。单声道数据的波形只有一条轨道而双声道交织数据能看到两条独立的轨道。如果两条轨道内容完全一样说明数据本身就是单声道复制出来的如果只有一条轨道有波形、另一条是直线说明驱动配置成了双声道但硬件只有单路有效信号。这一步能给后续方案提供很直接的判断依据。3.2 AI通道的声道配置与数据扩展逻辑确认现状之后我开始改AI侧。在Hi3536的SDK音频输入初始化接口中会传入一个音频采样参数结构体里面包含采样率、位深和声道数。先把声道数从1改成2再编译运行采集一段PCM数据看结果。正如前面说的因为硬件只有一路模拟输入双声道配置下右声道会采到接近静音的数据。这时需要在AI数据处理里加一个扩展逻辑把左声道数据和右声道数据做混合后复制到两个声道上。这个扩展逻辑的示意是这样的实际项目中你可以根据自己的位深和交织方式调整/* 单声道扩展为双声道基于交织16bit PCM */ void mono_to_dual(short *pcm_in, short *pcm_out, int frame_len) { for (int i 0; i frame_len / 2; i) { short sample (pcm_in[i * 2] pcm_in[i * 2 1]) / 2; pcm_out[i * 2] sample; pcm_out[i * 2 1] sample; } }注意这个例子默认输入已经是左右声道交织排列且左声道有数据、右声道接近静音。如果驱动或DMA配置的是非交织模式也就是一段数据全是左声道、紧接着一段全是右声道那处理方式就完全不同了需要按半帧为单位进行复制。判断交织和非交织的方法很简单采集一段已知只有左声道有信号的数据如果每两个采样点里第一个值接近有效幅度、第二个值接近0说明是交织如果连续几十个采样点都是有效值、然后连续几十个采样点都是近0值说明是非交织。这个判断错误会导致后面全部白做必须在一开始就确认清楚。3.3 AO侧的I2S与codec配置AI改完之后接着改AO。这里核心是I2S总线格式。I2S协议中左右声道的数据通过LRCK信号的电平进行区分两个声道共用一条数据线和一条时钟线。驱动初始化I2S时会设定数据格式是单声道还是双声道。单声道模式下驱动会忽略其中一个LRCK电平周期的数据只取另一个周期双声道模式下左右声道的采样点都会被打包进DMA传输。改AO的方式通常有两种。一种是在驱动初始化时把声道数配置成2让I2S驱动按双声道帧格式生成LRCK。另一种是保持I2S单声道配置在DMA搬运或送给codec之前做数据重新打包。第一种方式更干净但前提是你要能找到驱动里对应的格式配置结构体。我实际操作时会在驱动日志里加上一段初始化参数打印把声道数、位深、采样率、buffer大小全部打出来确认改动已经生效。有一点容易被忽略codec芯片本身也有左右声道的控制寄存器。声道数改成2之后要同步确认codec处在立体声模式而不是强制单声道输出模式。有的codec提供通道选择寄存器可以通过I2C或SPI总线配置如果这个寄存器值不对I2S链路输出的立体声数据到了codec那一步依然会被合成一路。调试时可以在codec驱动加载完成后通过驱动导出的debug文件查看各寄存器的值确认无误后再继续。3.4 编码器通道与封装信息同步修改AI和AO都改完只能保证设备本机录音和播放是双声道视频文件里的音轨属性未必跟着变。问题出在硬件编码器和封装模块。Hi3536平台的音视频编码走的是硬件编码器创建音频编码通道时传入的编码参数结构体里有声道数字段如果不改成2编码器就会把输入的双声道PCM按单声道节奏重新解析采样点错乱码流里记录的音轨属性也依然是1声道。这里要特别留意SDK示例代码里可能存在硬编码。我在项目中遇到的实际情况是驱动和编码器参数都已经改成2了但生成的MP4文件用播放器打开还是显示单声道。最后定位发现是示例工程里封装模块有一个单独定义的声道宏写死为1打包音轨头时用了这个宏。这种问题纯靠排查链路很难发现因为每个环节都认为是别人那边的问题。建议在验证时直接用播放器看最终文件的音频属性不要只看中间某个模块的日志否则极容易被误导。3.5 编译、烧录与三阶段验证代码改完之后编译内核音频驱动模块和SDK应用按平台标准方式烧录。我习惯分三步验证。第一步是设备本地播放测试准备一个左声道和右声道内容不同的立体声文件左声道是1kHz正弦波右声道是440Hz正弦波播放时仔细听两个喇叭或耳机的声音。如果左右两侧听到的声音不同说明AO链路正确如果两边听起来完全一样说明右声道数据在底层被丢弃或混合了。第二步是录音测试用板上麦克风录音生成PCM或WAV文件到PC上打开看波形轨道数量。如果能看到两条独立轨道且两条轨道内容一致或者接近一致说明AI链路的双声道数据处理生效。第三步是完整录像流程把音频编码、视频编码、音视频封装全部走一遍生成最终交付的媒体文件再用播放器和音频分析工具核对声道属性。这三步全部通过才算改造完成。4. 踩坑实录与问题排查技巧4.1 常见问题速查表整个改造过程中我陆续踩过不少坑也帮同事排查过类似问题。下面这张表整理了最常见的现象、直接原因和排查方向可以直接当作速查手册使用。现象直接原因排查方向系统声卡列表上报仍然是1声道驱动或设备树配置未生效检查模块是否重新编译加载、配置是否被覆盖播放时只有一侧有声音AO或I2S仍未按双声道配置检查LRCK波形、用左右不同内容测试文件录音文件在PC上显示单声道AI通道与编码器参数不一致同步检查AI参数和编码器声数字段声音明显失真且有规律爆音DMA的period/buffer大小未按新字节数重算计算双声道下每秒字节数重设DMA参数视频文件音轨显示为2声道但播放无声封装层写入的声道信息和实际数据不匹配重点排查封装模块中硬编码的声道宏接HDMI对端设备无声I2S改双声道后HDMI音频包结构随之变化评估HDMI音频通路的格式兼容性这里最让我印象深刻的是DMA周期大小导致爆音的问题。声道数从1变2数据传输量翻倍如果驱动里buffer_size和period_size还按单声道字节数计算一个中断周期内搬不完当前数据下一个周期的数据提前到来缓冲区就被覆盖结果就是持续不断的爆音。这种问题不会在启动阶段报错往往要播放一段时间才能听出来定位起来相当隐蔽。4.2 几个亲测有效的调试方法调试音频最有效的手段就是把PCM数据抓出来拿PC分析。设备端采集一段数据导出到主机用十六进制查看工具直接看采样点排列。16bit单声道数据一段连续的采样点都是有效值且每个采样点后紧跟着的下一个采样点也是同样值说明数据是重复的16bit交织双声道数据两个采样点为一组第一组里的两个数分别对应左右声道。通过统计前几十个采样点的规律基本就能判断数据格式对不对。如果平台走的是标准ALSA框架用ALSA工具查看声卡参数非常直观可以一次性拿到声道数、位深、采样率等信息。如果是SDK私有音频接口就在驱动代码里增加打印把初始化时收到的声道数和DMA配置都打出来。把驱动调试开关打开在AI和AO初始化时刻分别打印一次声道数、位深、采样率、buffer大小所有参数一目了然问题定位速度能提高不少。4.3 性能开销与长期稳定性的一点心得“单声道改双声道”听起来是数据量翻倍很多人担心CPU扛不住。实际测试下来Hi3536的音频处理吞吐量应付立体声绰绰有余48kHz采样率、16bit位深、双声道每秒钟的数据量大概是188KB左右这个量级对芯片来说完全不是压力。真正要留意的反而是内存带宽特别是有多路视频编码同时跑的设备多录一路立体声可能会和视频编码器抢DMA带宽。所以改造之后不要只测试音频功能要跑一遍整机满载的场景确认不影响视频流畅度。还有一个很容易被忽略的问题代码中所有与声道数相关的逻辑不只是一个初始化参数还有循环里的帧长度计算、DMA中断处理里的剩余数据量判断、编码器输入缓存的大小估算、上层缓冲区的申请全部要跟着改成2。我见过一种典型的低级错误初始化参数改成了双声道但DMA中断服务函数还在按单声道帧长度计算剩余数据量导致数据越攒越多、缓冲区溢出。这类问题要在老化和多路并发测试里才会暴露所以建议改完之后做至少12小时的连续播放录音测试中途反复切换音视频通道稳定性过关后再合入正式版本。考虑到后续产品线可能还会遇到类似需求我的体会是不要在驱动里把双声道写得太死最好把声道数做成一个运行时可配置的参数通过设备树或者模块加载参数传入。这样单声道设备和双声道设备可以共用一套驱动只是参数配置不同能省下大量重复劳动。我这次先按固定双声道实现随后又花时间改成可配置之后其他项目再碰到机器变体时基本只调参数不动代码复用效果非常明显。