1. 项目概述与核心需求解析在嵌入式开发尤其是基于STM32这类MCU的项目中ADC模数转换器的灵活运用是基本功。很多朋友都熟悉使用DMA直接存储器访问来搬运ADC的采样数据这确实能解放CPU实现高效、不间断的数据流。但一个常见的痛点也随之而来一旦初始化配置好ADC的采样通道序列Rank在程序运行过程中想要动态切换比如从测量一组电压切换到测量另一组传感器往往就卡壳了。提供的代码片段正是为了解决这个“动态改变ADC采样通道”的需求。简单来说我们面对的场景是一个STM32系统需要分时复用同一个ADC外设比如ADC1去采集多组不同的模拟信号。例如设备上电时可能需要监控电源电压VDC、VBAT而在执行某个特定功能时又需要去采集红外传感器IR的信号。如果为每一组信号都单独初始化一个ADC实例在资源紧张的MCU上既不现实也浪费。更优雅的做法是让同一个ADC硬件在不同的时间点听从我们的指令去采集不同的通道组合。传统的、一次性的HAL_ADC_ConfigChannel调用显然无法满足这个动态需求。从提供的代码里我们可以看到作者的核心思路在需要切换通道时先调用HAL_ADC_DeInit将ADC恢复到一个近乎初始的状态然后像初次初始化一样重新配置ADC公共参数和具体的通道序列。同时由于DMA的目标缓冲区大小和布局需要与通道数严格匹配所以启动DMA传输时也需要传入对应的缓冲区。这个思路直接、有效但里面藏着不少细节和“坑”接下来我们就把它掰开揉碎了讲清楚。2. 方案选型为何选择“DeInit 重配置”路径当我们需要动态改变ADC的采样通道时摆在面前的有几条路。理解为什么选择“DeInit 重配置”而不是其他看似更“轻量”的方法是写好这段代码的关键。2.1 潜在方案对比方案A动态修改SQR寄存器序列寄存器思路STM32的ADC有一个或多个序列寄存器SQR1, SQR2, SQR3直接决定了转换的通道和顺序。最理想的情况是在DMA搬运的间隙直接改写这些寄存器的值。现实对于STM32 HAL库尤其是配合DMA和扫描模式使用时这个操作风险极高。ADC的转换序列一旦启动其状态机是连续的、受DMA请求驱动的。贸然修改序列寄存器很可能导致DMA寻址错乱、数据错位甚至触发硬件错误。HAL库没有提供安全的、用于运行时修改序列的API自己操作寄存器需要对ADC和DMA的时序有极其精准的把握普适性差容易翻车。方案B配置多组通道用软件选择思路初始化时就把所有可能用到的通道比如CH1, CH5, CH6, CH7, CH12, CH13都配置进序列但通过软件触发或分析DMA数据时只取当前需要的那些通道的数据。缺点首先ADC的采样时间是累加的通道越多完成一次扫描循环的时间就越长这会降低有效采样率。其次这会无谓地消耗DMA缓冲区和CPU处理资源因为你需要搬运和处理大量无用数据。在需要高采样率或资源紧张的场合这不可接受。方案C停止-反初始化-重配置-启动即本文方案思路这是最“重”但也是最稳妥、最符合HAL库设计哲学的方法。先完全停止当前的ADC和DMA操作将ADC外设恢复到默认状态DeInit然后像初始化一个新ADC一样传入全新的配置包括通道数和每个通道的Rank最后重新启动。优势安全流程清晰完全遵循HAL库的生命周期管理Init - DeInit - Re-Init避免了硬件状态竞争。灵活不仅可以改通道还可以趁机修改采样时间、触发源、对齐方式等任何ADC参数。库友好完全利用标准HAL API代码可读性和可维护性高跨型号兼容性好。2.2 最终决策与核心考量显然方案C在可靠性、可维护性和兼容性上完胜。虽然它涉及一次“重启”操作会带来一个短暂的中断通常在微秒级但对于绝大多数分时复用场景如每秒切换几次采样目标来说这个开销完全可以接受。嵌入式开发中稳定性永远是第一位的。用一点可预测的、短暂的时间开销换取代码的健壮和清晰是非常划算的交易。提供的代码正是采用了方案C。ADC1_misc_Init和ADC1_ir_Init两个函数本质上就是两套不同的ADC配置。在需要切换时调用对应的初始化函数即可。这里的“Init”函数名可能有点误导它实际执行的是“Re-Initialization”。注意HAL_ADC_DeInit(hadc1)这个调用至关重要。它不仅仅是在软件层面重置句柄结构体更重要的是它会操作硬件寄存器将ADC外设置于复位状态并关闭相关时钟。如果没有这一步直接进行新的HAL_ADC_Init可能会因为硬件状态未清零而导致配置失败或行为异常。3. 关键代码解析与实操要点让我们深入分析提供的代码理解每一部分的意图并指出其中需要特别注意的地方。3.1 ADC配置结构体与初始化流程void ADC1_misc_Init(void) { ADC_ChannelConfTypeDef sConfig; // 第一步反初始化 HAL_ADC_DeInit(hadc1); /**Common config */ hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; // 启用扫描模式 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换模式 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; // 数据右对齐 hadc1.Init.NbrOfConversion 4; // 关键本次配置的通道总数 if (HAL_ADC_Init(hadc1) ! HAL_OK) { _Error_Handler(__FILE__, __LINE__); } // ... 后续配置具体通道 }ScanConvMode ENABLE这是多通道DMA采样的前提。扫描模式允许ADC按序列自动转换多个通道。ContinuousConvMode ENABLE连续转换模式。一旦启动ADC会不停地按照序列循环转换。这对于DMA持续搬运数据是必要的。NbrOfConversion 4这是动态切换的核心参数之一。它必须与你接下来实际配置的通道数量严格一致。在ADC1_ir_Init函数中这个值被设置为2。如果这里填错会导致DMA传输长度计算错误引发数据覆盖或内存访问越界等严重问题。3.2 通道配置与Rank的重要性/**Configure Regular Channel */ sConfig.Channel ADC_CHANNEL_12; // bdc sConfig.Rank ADC_REGULAR_RANK_1; // 转换顺序中的第1位 sConfig.SamplingTime ADC_SAMPLETIME_1CYCLE_5; // 通道12的采样时间 if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { _Error_Handler(__FILE__, __LINE__); } // 重复配置 Rank 2, Rank 3, Rank 4...Rank等级定义了该通道在转换序列中的位置。ADC_REGULAR_RANK_1是第一个被转换的RANK_2是第二个以此类推。DMA搬运的数据在内存中的排列顺序就是由Rank顺序决定的。第一个DMA目标地址存放的是Rank1通道的结果第二个地址存放Rank2的结果。SamplingTime采样时间。这里只有第一个通道Rank1显式设置了采样时间。在提供的代码中后续通道复用了sConfig结构体但没有重新赋值SamplingTime。这是一个隐患在HAL库中HAL_ADC_ConfigChannel函数会使用sConfig结构体当前的所有成员值。如果上一个通道的采样时间很长例如ADC_SAMPLETIME_480CYCLES而下一个通道需要高速采样这里就会出错。实操心得务必为每个通道独立、完整地配置sConfig结构体。即使某些参数如采样时间相同也显式地写出来。或者在每次调用HAL_ADC_ConfigChannel前重新初始化整个sConfig变量。良好的习惯是ADC_ChannelConfTypeDef sConfig {0}; // 每次循环或调用前清零 sConfig.Channel ...; sConfig.Rank ...; sConfig.SamplingTime ...; // 每个通道都明确指定 HAL_ADC_ConfigChannel(...);3.3 DMA缓冲区与启动函数的匹配这是动态切换中最容易出错的一环。void StartADC1Misc(void){ HAL_ADC_Start_DMA(hadc1, ADC1Buf.ValMem, ADCCNT*4); // 注意这里是 *4 } void StartADC1IR(void){ HAL_ADC_Start_DMA(hadc1, IRBuf.ValMem, ADCCNT*2); // 注意这里是 *2 }HAL_ADC_Start_DMA的第三个参数Length表示DMA要传输的数据单元总数。一个“数据单元”对应一个ADC转换结果通常是16位或32位取决于对齐方式。匹配原则Length必须等于每次扫描的通道数 (NbrOfConversion)×你想存储的扫描轮数 (ADCCNT)。在misc配置中通道数为4所以是ADCCNT * 4。DMA会将4个通道的结果作为一组连续地搬运到ADC1Buf.ValMem。在ir配置中通道数为2所以是ADCCNT * 2。DMA搬运的是2个通道为一组的数据到IRBuf.ValMem。缓冲区定义代码中ADC1Buf和IRBuf应该是用户自定义的结构体或数组。它们的内存布局必须与DMA的搬运方式匹配。从后面的AdcGetValue函数推测ADC1Buf.Val可能被定义为一个二维数组[ADCCNT][4]而IRBuf.Val则是[ADCCNT][2]。这种隐式的对应关系需要你在编程时非常小心地维护。严重警告如果Length参数传错比如在ir模式下传了ADCCNT*4DMA会忠实地搬运超出IRBuf实际内存范围的数据导致内存踩踏破坏其他变量造成各种难以调试的随机性故障。这是嵌入式系统崩溃的常见原因之一。3.4 数据读取与平均滤波uint32_t AdcGetValue(uint8_t ch){ uint32_t temp,i; temp 0; for(i ADCCNT/2-5; i ADCCNT/25; i){ // 取中间部分数据 temp temp (ADC1Buf.Val[i][ch] 0xFFF); // 取低12位有效数据 } temp / 10; // 计算10个数据的平均值 return temp; }数据对齐 0xFFF操作是因为ADC配置为ADC_DATAALIGN_RIGHT右对齐。对于12位ADC有效数据在低12位高20位可能是0或不确定值。这个操作确保了数据的正确性。软件滤波函数没有简单地取最新值或第一个值而是从缓冲区中部ADCCNT/2附近取10个连续样本做平均。这是一种简单有效的去噪和消除启动瞬态影响的方法。因为ADC和DMA刚启动时前几次转换可能不稳定取中间段的数据更可靠。索引ch这里的ch参数对应的是通道在当前序列中的Rank索引从0开始而不是ADC的物理通道号。AdcGetValue(0)取的是Rank1CH12的数据AdcGetValue(1)取的是Rank2CH13的数据。这个映射关系必须在你的应用层代码中牢记。4. 完整实现流程与步骤拆解基于以上分析我们可以梳理出一套安全、完整的动态切换ADC通道的实操流程。假设我们有两个任务任务A采样4个通道任务B采样2个通道。4.1 准备工作定义缓冲区与全局句柄// 1. 定义ADC句柄通常放在main.c或专用外设文件 ADC_HandleTypeDef hadc1; // 2. 根据最大可能通道数合理定义缓冲区大小 #define MAX_ADC_CHANNELS 4 #define ADC_SAMPLE_ROUNDS 100 // 缓存100轮扫描数据 // 任务A的缓冲区假设是4通道 typedef struct { uint16_t Val[ADC_SAMPLE_ROUNDS][4]; // 二维数组[轮次][通道索引] uint32_t ValMem[ADC_SAMPLE_ROUNDS * 4]; // 一维数组供DMA直接使用 } ADC_Buffer_T; // 任务B的缓冲区假设是2通道 typedef struct { uint16_t Val[ADC_SAMPLE_ROUNDS][2]; uint32_t ValMem[ADC_SAMPLE_ROUNDS * 2]; } IR_Buffer_T; ADC_Buffer_T adcBufferA; IR_Buffer_T adcBufferB; // 3. 确保一维和二维数组在内存上对齐通常编译器会保证但需知晓 // adcBufferA.Val[0][0] 应该等于 adcBufferA.ValMem[0]4.2 核心编写通道配置函数/** * brief 配置ADC1用于采集任务A的通道CH12, CH13, CH1, CH7 * note 此函数会执行DeInit调用前需确保ADC已停止 */ void ADC_Config_For_TaskA(void) { ADC_ChannelConfTypeDef sConfig {0}; // 关键步骤1反初始化重置硬件状态 HAL_ADC_DeInit(hadc1); // 关键步骤2配置ADC公共参数 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 4; // 任务A有4个通道 hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; // 新增建议明确时钟分频 hadc1.Init.Resolution ADC_RESOLUTION_12B; // 新增明确分辨率 if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } // 关键步骤3逐个配置通道注意Rank顺序 // 通道1 (Rank1) sConfig.Channel ADC_CHANNEL_12; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_15CYCLES; // 根据信号源阻抗选择合适的采样时间 sConfig.SingleDiff ADC_SINGLE_ENDED; sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); } // 通道2 (Rank2) - 重新初始化sConfig是良好习惯 sConfig.Channel ADC_CHANNEL_13; sConfig.Rank ADC_REGULAR_RANK_2; sConfig.SamplingTime ADC_SAMPLETIME_15CYCLES; // 再次明确指定 // ... 其他参数保持或重置 if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); } // ... 类似地配置 CH1 (Rank3) 和 CH7 (Rank4) } /** * brief 配置ADC1用于采集任务B的通道CH5, CH6 */ void ADC_Config_For_TaskB(void) { ADC_ChannelConfTypeDef sConfig {0}; HAL_ADC_DeInit(hadc1); // 同样先反初始化 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode ENABLE; // ... 其他参数 hadc1.Init.NbrOfConversion 2; // 这里变成了2 if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } // 配置通道 CH5 (Rank1) sConfig.Channel ADC_CHANNEL_5; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_3CYCLES; // 可能任务B需要更快的采样 // ... if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); } // 配置通道 CH6 (Rank2) sConfig.Channel ADC_CHANNEL_6; sConfig.Rank ADC_REGULAR_RANK_2; sConfig.SamplingTime ADC_SAMPLETIME_3CYCLES; // ... if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); } }4.3 控制流程在应用中安全切换void TaskA_Measurement_Routine(void) { // 1. 确保当前没有ADC/DMA在运行如果是连续模式需要先停止 HAL_ADC_Stop_DMA(hadc1); // 2. 重新配置为任务A的通道 ADC_Config_For_TaskA(); // 3. 启动DMA传入任务A的缓冲区和正确的长度 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBufferA.ValMem, ADC_SAMPLE_ROUNDS * 4); // 4. 等待DMA搬运足够的数据可以通过DMA半满/全满中断或简单延时 HAL_Delay(10); // 示例等待10ms假设采样率足够填充缓冲区 // 5. 处理数据 uint32_t vdc AdcGetValueFromBuffer(adcBufferA, 0); // 获取Rank1数据 uint32_t vbat AdcGetValueFromBuffer(adcBufferA, 1); // 获取Rank2数据 // ... 使用数据 } void TaskB_Measurement_Routine(void) { // 1. 停止当前ADC/DMA HAL_ADC_Stop_DMA(hadc1); // 2. 重新配置为任务B的通道 ADC_Config_For_TaskB(); // 这个函数内部会调用DeInit // 3. 启动DMA传入任务B的缓冲区和正确的长度 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBufferB.ValMem, ADC_SAMPLE_ROUNDS * 2); // 4. 等待并处理数据 HAL_Delay(10); uint32_t pos AdcGetIRValueFromBuffer(adcBufferB, 0); uint32_t bubble AdcGetIRValueFromBuffer(adcBufferB, 1); // ... 使用数据 }5. 常见问题、调试技巧与深度优化即使按照上述流程操作在实际项目中你还是会遇到各种问题。下面是我在多个项目中总结出来的“避坑指南”。5.1 DMA传输完成中断TC与半传输中断HT的妙用在提供的示例中使用延时HAL_Delay等待数据就绪。这在简单应用中可行但极不精确且低效。最佳实践是使用DMA中断。配置在HAL_ADC_Start_DMA之前使能DMA的传输完成中断TC和半传输中断HT。原理DMA在搬运完一半数据HT和全部数据TC时会产生中断。你可以在中断回调函数中设置标志位。双缓冲区技巧这是ADC-DMA采样的经典模式。在HT中断时处理前半部分缓冲区数据在TC中断时处理后半部分缓冲区数据。这样数据处理和DMA搬运可以并行几乎可以实现“实时”处理且没有数据覆盖的风险因为DMA总是在向另一个“半区”写入。// 在main.c或stm32fxx_it.c中 void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(hdma_adc1); } // 在ADC初始化后注册回调函数HAL库方式 // 需要重写 __weak 定义的 HAL_ADC_ConvCpltCallback 和 HAL_ADC_ConvHalfCpltCallback void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { // 设置“缓冲区满”标志通知主循环或任务处理 adcBuffer 的后半部分 adc_data_ready_full_flag 1; } } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { // 设置“半缓冲区满”标志通知处理 adcBuffer 的前半部分 adc_data_ready_half_flag 1; } }5.2 采样率计算与缓冲区大小设计这是一个容易被忽略但至关重要的点。你的ADC_SAMPLE_ROUNDS代码中的ADCCNT和采样率直接相关。单次扫描时间 Σ(每个通道的采样时间 转换时间) 额外开销。12位转换时间通常是固定的例如在STM32F4上约需3个ADC时钟周期。采样时间取决于你设置的SamplingTime如15个周期。扫描周期 单次扫描时间。在连续模式下ADC会以这个周期不断循环。DMA缓冲区填满时间ADC_SAMPLE_ROUNDS× 扫描周期。设计原则缓冲区大小要足够容纳你处理数据所需的时间。例如如果你每10ms处理一次数据那么缓冲区填满时间应略小于10ms以确保每次处理时都有新数据。如果缓冲区太大数据延迟高太小则可能来不及处理就被覆盖。5.3 通道切换时的“毛刺”与稳定等待调用HAL_ADC_DeInit再Init会导致ADC时钟关闭再开启模拟电路部分会有个重新稳定的过程。切换通道后立即采样前几个数据很可能是不准确的。解决方案软件丢弃像示例代码AdcGetValue那样不取缓冲区最开始的若干组数据而是从中间开始取。这是最简单有效的方法。硬件稳定延时在HAL_ADC_Start_DMA之后主动等待一小段时间例如执行几个空循环或短延时让ADC模拟前端稳定再开始正式使用数据。校准如果支持有些STM32的ADC支持自校准。可以在每次HAL_ADC_Init之后调用HAL_ADCEx_Calibration_Start进行校准但这会增加切换时间。5.4 多任务/中断环境下的同步问题如果你的TaskA_Measurement_Routine和TaskB_Measurement_Routine可能被中断打断或者在RTOS的不同任务中调用那么重入问题就来了。风险任务A正在执行ADC_Config_For_TaskA刚完成DeInit突然被中断打断中断服务程序调用了TaskB_Measurement_Routine。此时ADC被重新配置为任务B的模式当中断返回任务A继续执行HAL_ADC_Init时配置就会混乱。解决对ADC配置和启动过程加锁。可以使用关中断、调度器锁在RTOS中或简单的信号量/互斥量来确保ADC配置序列的原子性。// 伪代码以RTOS FreeRTOS为例 SemaphoreHandle_t xADCMutex; void TaskA_Measurement_Routine(void) { if(xSemaphoreTake(xADCMutex, portMAX_DELAY) pdTRUE) { HAL_ADC_Stop_DMA(hadc1); ADC_Config_For_TaskA(); HAL_ADC_Start_DMA(hadc1, ...); xSemaphoreGive(xADCMutex); } // ... 等待并处理数据 }5.5 功耗考量连续转换模式ContinuousConvMode ENABLE下ADC会一直工作功耗较高。如果设备对功耗敏感且采样是间歇性的可以考虑以下优化使用单次转换模式ContinuousConvMode DISABLE。每次需要采样时调用HAL_ADC_Start_DMADMA会在转换完指定数量的数据后自动停止。在动态切换场景下这很合适因为每次任务都会重新启动。动态开关ADC时钟在长时间不采样时可以在DeInit之后Init之前通过__HAL_RCC_ADC1_CLK_DISABLE()关闭ADC时钟以省电。但要注意开关时钟也有稳定时间开销。6. 进阶更灵活的通道管理方案对于通道组合非常多变的复杂应用上述为每种组合写一个配置函数的方式会显得冗余。我们可以设计一个更通用的通道配置函数。typedef struct { uint32_t Channel; uint32_t Rank; uint32_t SamplingTime; } ADC_ChannelConfig_t; /** * brief 通用ADC动态重配置函数 * param pChConfig: 通道配置结构体数组 * param numCh: 通道数量 * retval HAL status */ HAL_StatusTypeDef ADC_Reconfigure_Dynamic(ADC_HandleTypeDef* hadc, ADC_ChannelConfig_t* pChConfig, uint32_t numCh) { HAL_StatusTypeDef status; ADC_ChannelConfTypeDef sConfig {0}; // 1. 停止并反初始化 HAL_ADC_Stop_DMA(hadc); HAL_ADC_DeInit(hadc); // 2. 修改公共参数中的通道数 hadc-Init.NbrOfConversion numCh; // 注意这里假设hadc-Init的其他成员如ScanConvMode, ContinuousConvMode等已经预先配置好且不变。 // 如果这些也需要动态改则需要作为参数传入。 status HAL_ADC_Init(hadc); if(status ! HAL_OK) return status; // 3. 循环配置所有通道 for(uint32_t i 0; i numCh; i) { sConfig.Channel pChConfig[i].Channel; sConfig.Rank pChConfig[i].Rank; sConfig.SamplingTime pChConfig[i].SamplingTime; sConfig.SingleDiff ADC_SINGLE_ENDED; sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; status HAL_ADC_ConfigChannel(hadc, sConfig); if(status ! HAL_OK) return status; } return HAL_OK; } // 使用示例 ADC_ChannelConfig_t taskA_channels[] { {ADC_CHANNEL_12, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_15CYCLES}, {ADC_CHANNEL_13, ADC_REGULAR_RANK_2, ADC_SAMPLETIME_15CYCLES}, {ADC_CHANNEL_1, ADC_REGULAR_RANK_3, ADC_SAMPLETIME_15CYCLES}, {ADC_CHANNEL_7, ADC_REGULAR_RANK_4, ADC_SAMPLETIME_15CYCLES}, }; void Switch_To_TaskA(void) { if(ADC_Reconfigure_Dynamic(hadc1, taskA_channels, 4) HAL_OK) { HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBufferA.ValMem, ADC_SAMPLE_ROUNDS * 4); } }这个方案将通道配置数据化大大提高了代码的复用性和可维护性。你可以把不同的通道组合定义成数组切换时只需调用这个通用函数并传入对应的数组和长度即可。