梁山派GD32F470光敏电阻传感器驱动移植实战ADC与DMA采集光照强度最近在梁山派GD32F470开发板上做环境监测项目需要检测光照强度就用到了最常见的光敏电阻模块。很多刚开始接触嵌入式传感器开发的朋友可能会觉得ADC采集、DMA传输这些概念有点复杂配置起来也容易出错。今天我就结合这个光敏电阻模块手把手带大家走一遍完整的驱动移植流程从硬件原理分析到代码编写最后实现光照强度的百分比显示。整个过程我会尽量讲得详细把容易踩的坑也指出来保证你跟着做一遍就能完全掌握。1. 光敏电阻模块与硬件连接咱们先来认识一下今天的主角——光敏电阻传感器模块。1.1 模块工作原理光敏电阻简单来说就是一种“见光变怂”的电阻。它内部用的是硫化镉或硒化镉这类半导体材料其核心特性是光照越强电阻值越小完全黑暗时电阻值非常大。这背后的原理叫做“内光电效应”。当有光线照射时半导体材料内部会产生可以自由移动的电荷载流子这些电荷在外部电压的驱动下定向移动就形成了电流。电荷多了导电能力就强了电阻自然就小了。我们用的这个模块型号是5516根据它的资料在明亮环境下电阻大约在8-20KΩ而在黑暗环境中电阻可以高达1MΩ左右。1.2 模块接口与引脚这个模块非常小巧尺寸是31.1mm x 14.1mm采用4Pin2.54mm间距排针供电和通信。工作电压是3.3V-5V和咱们的梁山派开发板3.3V系统完美匹配。模块上有两个关键输出引脚功能完全不同引脚标识类型功能说明VCC电源接3.3V或5VGND电源接地AO模拟输出输出连续变化的电压信号需要接MCU的ADC引脚DO数字输出输出高低电平需要接MCU的GPIO输入引脚AO引脚是模拟量输出。它直接是光敏电阻和一个固定电阻原理图里的R2和R3分压后的电压点。光线变化导致光敏电阻阻值变化分压点的电压也就连续变化。这个电压值被送到单片机的ADC引脚转换成数字值我们就能精确知道当前的光照强度。DO引脚是数字量输出。它连接了一个电压比较器芯片LM393。比较器会持续比较AO引脚的电压和一个可调参考电压通过电位器R4设置。当AO电压高于参考电压比如光线变暗DO输出高电平反之输出低电平。这个引脚适合做简单的“亮/暗”阈值判断比如天黑自动开灯。注意对于需要量化光照强度的应用我们必须使用AO引脚配合ADC进行采集。DO引脚只能做二值判断。1.3 与梁山派开发板连接我们的目标是采集AO引脚的模拟电压。首先得在梁山派GD32F470上找一个带ADC功能的引脚。查阅GD32F470的数据手册具体是GD32F450xx_Datasheet_Rev2.2.pdf第28页的引脚功能定义图可以看到很多引脚都支持ADC。这里我选择使用PC1引脚它复用了ADC0的第11输入通道。连接非常简单模块VCC- 开发板3.3V模块GND- 开发板GND模块AO- 开发板PC1模块DO- 开发板任意GPIO输入引脚例如PA12. 驱动程序设计ADC与DMA配置接下来是核心部分——编写驱动程序。为了让ADC采集不占用CPU时间我们采用“ADC DMA”的方案。ADC负责把电压变成数字DMA负责悄无声息地把这个数字搬运到我们指定的内存数组中CPU完全不用管。2.1 头文件定义 (bsp_illume.h)头文件里主要做宏定义把引脚、外设通道等配置信息固定下来这样程序看起来清晰修改也方便。#ifndef _BSP_ILLUME_H_ #define _BSP_ILLUME_H_ #include gd32f4xx.h // 时钟定义 #define RCU_ILLUME_GPIO_AO RCU_GPIOC // AO引脚(PC1)的GPIO时钟 #define RCU_ILLUME_GPIO_DO RCU_GPIOA // DO引脚(PA1)的GPIO时钟 #define RCU_ILLUME_ADC RCU_ADC0 // ADC0时钟 #define RCU_ILLUME_DMA RCU_DMA1 // DMA1时钟 // DMA与ADC外设定义 #define PORT_DMA DMA1 // 使用DMA1 #define CHANNEL_DMA DMA_CH0 // 使用DMA1的通道0 #define PORT_ADC ADC0 // 使用ADC0 #define CHANNEL_ADC ADC_CHANNEL_11 // 使用ADC0的第11通道(对应PC1) // GPIO引脚定义 #define PORT_ILLUME_AO GPIOC // AO引脚端口 #define GPIO_ILLUME_AO GPIO_PIN_1 // AO引脚号(PC1) #define PORT_ILLUME_DO GPIOA // DO引脚端口 #define GPIO_ILLUME_DO GPIO_PIN_1 // DO引脚号(PA1) // 读取DO引脚电平的快捷宏 #define GET_DO_IN gpio_input_bit_get(PORT_ILLUME_DO, GPIO_ILLUME_DO) // 采样参数定义 #define SAMPLES 30 // 每个通道采样30次用于后续求平均滤波 #define CHANNEL_NUM 1 // 总共采样1个ADC通道 // 声明DMA使用的全局缓冲区数组 extern uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; // 函数声明 void Illume_GPIO_Init(void); unsigned int Get_Adc_Dma_Value(char CHx); unsigned int Get_illume_Percentage_value(void); char Get_DO_In(void); #endif2.2 初始化函数详解 (bsp_illume.c)初始化函数Illume_GPIO_Init是整个驱动的核心它完成了从时钟使能到启动ADC转换的所有配置。咱们一步步拆解来看。第一步开启所有需要的时钟就像用电器要先通电一样使用任何外设前都要先打开它的时钟。rcu_periph_clock_enable(RCU_ILLUME_GPIO_AO); // 打开GPIOC时钟 rcu_periph_clock_enable(RCU_ILLUME_GPIO_DO); // 打开GPIOA时钟 rcu_periph_clock_enable(RCU_ILLUME_ADC); // 打开ADC0时钟 rcu_periph_clock_enable(RCU_ILLUME_DMA); // 打开DMA1时钟第二步配置ADC时钟和GPIO模式ADC工作需要基准时钟这里设置为APB2时钟PCLK2的4分频。GPIO模式是关键AO引脚必须设置为模拟输入模式。adc_clock_config(ADC_ADCCK_PCLK2_DIV4); // 配置ADC时钟源 // DO引脚(PA1)配置为上拉/下拉无效的输入模式用于读取高低电平 gpio_mode_set(PORT_ILLUME_DO, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_ILLUME_DO); // AO引脚(PC1)必须配置为模拟输入模式这是ADC采集的正确姿势 gpio_mode_set(PORT_ILLUME_AO, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_ILLUME_AO);第三步配置ADC工作模式这里配置了ADC以独立、连续、扫描模式工作数据右对齐并开启了DMA请求。adc_sync_mode_config(ADC_SYNC_MODE_INDEPENDENT); // ADC独立模式 adc_special_function_config(PORT_ADC, ADC_CONTINUOUS_MODE, ENABLE); // 使能连续转换 adc_special_function_config(PORT_ADC, ADC_SCAN_MODE, ENABLE); // 使能扫描模式多通道时才需 adc_data_alignment_config(PORT_ADC, ADC_DATAALIGN_RIGHT); // 数据右对齐方便读取 adc_resolution_config(PORT_ADC, ADC_RESOLUTION_12B); // 12位分辨率0-4095 // 设置规则组通道数量我们只用1个通道和具体通道参数 adc_channel_length_config(PORT_ADC, ADC_REGULAR_CHANNEL, CHANNEL_NUM); // 配置规则组第0个转换序列为ADC通道11采样时间15个周期 adc_regular_channel_config(PORT_ADC, 0, CHANNEL_ADC, ADC_SAMPLETIME_15); // 禁用外部触发使用软件触发启动转换 adc_external_trigger_config(PORT_ADC, ADC_REGULAR_CHANNEL, EXTERNAL_TRIGGER_DISABLE); // 使能每次规则组转换完成后产生DMA请求 adc_dma_request_after_last_enable(PORT_ADC); adc_dma_mode_enable(PORT_ADC); // 使能ADC的DMA模式第四步ADC校准ADC模块内部有微小误差上电后进行一次校准可以大大提高采集精度这是很重要但容易被忽略的一步。adc_enable(PORT_ADC); // 先使能ADC delay_1ms(1); // 等待至少1ms让ADC稳定 adc_calibration_enable(PORT_ADC); // 开始自校准第五步配置DMA搬运工DMA的配置是重点它决定了数据从哪里搬到哪里怎么搬。dma_deinit(PORT_DMA, CHANNEL_DMA); // 先清除DMA通道之前的配置 dma_single_data_parameter_struct dma_single_data_parameter; // 设置源地址ADC的数据寄存器地址数据从这里被搬出 dma_single_data_parameter.periph_addr (uint32_t)(ADC_RDATA(PORT_ADC)); dma_single_data_parameter.periph_inc DMA_PERIPH_INCREASE_DISABLE; // 外设地址固定不自增 // 设置目标地址我们定义的全局数组 gt_adc_val 的首地址 dma_single_data_parameter.memory0_addr (uint32_t)(gt_adc_val); dma_single_data_parameter.memory_inc DMA_MEMORY_INCREASE_ENABLE; // 内存地址自增存到数组下一个位置 // 设置传输参数 dma_single_data_parameter.periph_memory_width DMA_PERIPH_WIDTH_16BIT; // 每次传输16位ADC数据是12位用16位变量存 dma_single_data_parameter.direction DMA_PERIPH_TO_MEMORY; // 传输方向从外设到内存 dma_single_data_parameter.number SAMPLES * CHANNEL_NUM; // 总传输次数 30次 * 1通道 dma_single_data_parameter.priority DMA_PRIORITY_HIGH; // 高优先级 // 初始化DMA通道 dma_single_data_mode_init(PORT_DMA, CHANNEL_DMA, dma_single_data_parameter);第六步选择DMA通道映射并启动GD32的DMA通道需要和外设这里是ADC0绑定。// 根据数据手册ADC0对应DMA_SUBPERI0 dma_channel_subperipheral_select(PORT_DMA, CHANNEL_DMA, DMA_SUBPERI0); dma_circulation_enable(PORT_DMA, CHANNEL_DMA); // 使能循环模式传输完自动从头开始 dma_channel_enable(PORT_DMA, CHANNEL_DMA); // 使能DMA通道 // 最后软件触发ADC开始转换。一旦开始ADC会连续转换DMA会连续搬运完全自动运行。 adc_software_trigger_enable(PORT_ADC, ADC_REGULAR_CHANNEL);初始化完成后ADC就会以设定的速率持续采样PC1引脚的电压并通过DMA自动将结果存放到gt_adc_val数组中整个过程无需CPU干预。2.3 数据读取与处理函数数据已经在后台自动采集好了我们只需要从数组中读取并处理。获取ADC原始值并求平均为了滤除偶然干扰我们采集了30个样本求取平均值作为最终结果。unsigned int Get_Adc_Dma_Value(char CHx) { unsigned char i 0; unsigned int AdcValue 0; // 累加 SAMPLES 次采样的数据 for(i0; i SAMPLES; i) { AdcValue gt_adc_val[i][CHx]; // CHx为通道索引我们只有0通道 } // 求平均值 AdcValue AdcValue / SAMPLES; return AdcValue; // 返回值范围0 ~ 4095 }将ADC值转换为光照百分比这是将物理量转换为直观数值的关键一步。注意光敏电阻的特性是光照越强电阻越小分压后AO引脚电压越低ADC值越小。所以我们需要用“1 - 比值”来反映正相关。unsigned int Get_illume_Percentage_value(void) { int adc_max 4095; // 12位ADC的最大值 int adc_new 0; int Percentage_value 0; adc_new Get_Adc_Dma_Value(0); // 获取当前ADC平均值 // 核心转换公式百分比 ( 1 - (当前值 / 最大值) ) * 100 // 当 adc_new0最亮百分比为100%当 adc_new4095最暗百分比为0% Percentage_value ( 1 - ( (float)adc_new / adc_max ) ) * 100; return Percentage_value; // 返回0~100之间的整数 }读取DO引脚状态这个函数很简单就是读取连接DO引脚的GPIO电平。char Get_DO_In(void) { if( GET_DO_IN 1 ) // 使用头文件定义的宏读取引脚 { return 1; // 返回1表示光线低于阈值可能过暗 } return 0; // 返回0表示光线高于阈值 }3. 在main函数中测试验证驱动写好了最后就是在主函数里调用测试了。#include gd32f4xx.h #include systick.h #include bsp_usart.h #include bsp_illume.h #include stdio.h int main(void) { // 系统初始化 nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 设置中断优先级分组 systick_config(); // 初始化系统滴答定时器用于delay usart_gpio_config(115200U); // 初始化串口用于打印数据 Illume_GPIO_Init(); // 初始化光敏电阻模块 printf(ADCDMA Light Sensor Demo Start\r\n); while(1) { // 打印ADC原始值0-4095 printf(ADC Value: %d\r\n, Get_Adc_Dma_Value(0)); // 打印光照强度百分比0%-100% printf(Light Intensity: %d%%\r\n, Get_illume_Percentage_value()); // 打印DO引脚状态 printf(DO Pin State: %s\r\n, Get_DO_In() ? Dark : Bright); delay_1ms(1000); // 每隔1秒打印一次 } }将代码编译下载到梁山派开发板打开串口助手波特率115200你就能看到实时的光照数据输出了。用手遮住光敏电阻或者用手电筒照它观察ADC值和百分比的变化。当光线变化时DO引脚的状态也会在某个阈值点发生翻转。4. 常见问题与调试心得ADC采样值跳动大原因可能是电源噪声或信号干扰。解决代码里已经做了软件平均滤波采样30次求平均能有效平滑数据。如果还不行可以在AO引脚和GND之间加一个0.1uF的滤波电容。百分比计算不准确注意公式逻辑我们的公式(1 - (adc/4095)) * 100是基于该模块电路光敏电阻在上拉位置的。如果你的模块电路不同比如光敏电阻在下拉位置公式可能要改成(adc/4095) * 100。一定要根据实际电路的分压关系来定。DMA数据不更新检查顺序务必确保初始化顺序是ADC校准 - DMA配置 - 启动DMA - 软件触发ADC。顺序错了DMA可能不工作。检查缓冲区确保gt_adc_val数组被正确定义为全局变量且长度足够SAMPLES * CHANNEL_NUM。DO引脚阈值怎么调模块上通常有一个蓝色的电位器就是原理图里的R4用螺丝刀旋转它就可以改变LM393比较器的参考电压从而改变光线触发阈值。调到合适位置就能实现“天暗到一定程度才输出高电平”的效果。好了整个光敏电阻的ADCDMA驱动移植过程就完成了。最关键的是理解“ADC负责转换DMA负责搬运”这个协作流程。一旦掌握了这个模式其他类似的传感器比如土壤湿度、气体浓度等的驱动开发就都是类似的思路了。