一、前言

这是实验室项目要求实现的一个小功能,自己又想试一下写博客,都说有帮助,所以打算试一下,如有错误请指正!谢谢大家!

并且我发现CSDN的各种标题都长得差不多,可能看着会很混乱,强烈推荐配合目录食用!

实现的功能

亮度或光照变暗时,WS2812或者LED自动变亮,其中LED可以实现随光照强度自动调整亮度。

二、使用到的核心模块

STM32F103C8T6、WS2812、LED、光敏传感器。

光敏传感器

检测环境亮暗,光越强电阻越小,输出电压变化。有AO 模拟量(测亮度)和DO 数字量(亮 / 暗开关),用来做光控灯、亮度检测。

配合LED时:使用四引脚光敏传感器,VCC与GND正常接入面包板,AO(模拟输出,配合ADC使用)接入ADC指定的IO引脚,引脚持续获取光敏传感器输出的ADC值,达到自己设置的阈值触发代码,对LED进行点亮并根据光敏传感器输出的ADC时时调整亮度。

WS2812模块

WS2812 就是自带驱动芯片的 RGB 灯珠。一根信号线就能串很多个,发不同数据就能亮任意颜色,时序要求很严,一般用 PWM+DMA 驱动。若是多个WS2812级联,上一个WS2812的DO接入下一个WS2812的DI。

三、开发环境

STM32cubeMX+keil5,使用HAL库开发,下载工具为ST-Link。

四、实验1原理(WS2812+光敏传感器)

光敏传感器+WS2812实现天暗自动开灯

光敏传感器作用及其运作原理

使用四引脚光敏传感器,VCC与GND正常接入面包板,DO(数字输出,可通过电位器调节阈值)接入设定IO引脚,引脚检测到电平变化后,触发相应代码,则对WS2812进行点亮或者实现其他功能。

WS2812作用及其运作原理

VCC接5V(但是我自己实测接3.3V也没问题),GND接GND,DIN接设定的IO引脚。

使用TIM生成PWM+DMA的形式对WS2812实现控制。并且WS2812有 0码、1码和reset 三种输入信号的概念,可以理解为专属于WS2812的低电平0和高电平1,以此来对WS2812模块进行编程,可以通过特殊方式(下面的原理讲)给WS2812生成0码和1码和reset。

WS2812原理:在指定计数周期下(1.25us)不同的高电平信号时长能输出0码、1码和reset信号。

简单记:短高电平 = 0;长高电平 = 1;长低电平 = 复位。

WS2812运作原理:每颗灯珠能按照G, R, B(绿红蓝)三种颜色,组合实现炫彩的灯光效果。而要修改其色彩,需要对其输入24个bit长度的0码/1码信号进行调整。严格按照G, R, B(绿红蓝)顺序,每种颜色需8个字节长度的0码/1码信号作为颜色数据,3种颜色即需要24位。

例如:红色(1111 1111)绿色(0000 0000)蓝色(0000 0000)=红色(255,0,0)

红色(1111 1111)绿色(1111 1111)蓝色(1111 1111)=白色(255,255,255)

关于如何调整颜色数据,因为涉及到一个计数周期内的高电平时长(0码/1码的设置),我们考虑使用PWM波,通过调节PWM波的占空比,实现调整一个时钟周期内的高电平时长,从而实现输出0码/1码/reset信号的目的。

至于使用DMA,是因为WS2812对时序要求十分严格,cpu手动控制时,可能一个小小的中断,甚至可能是因为cpu慢了一点,就会导致时序错误;其次,不使用DMA,对cpu占用严重,cpu就得一直盯着翻转电平,严重干扰cpu执行其他任务。用了 DMA,CPU 把 “发灯珠数据” 这个活交给 DMA 后,自己就能去处理其他任务,直接提高效率。

一段话总结

本项目通过四引脚光敏传感器检测环境亮度,光敏电阻随光线强弱改变阻值,经模块比较器从 DO 引脚输出高低电平来判断天黑或天亮,电平变化触发单片机动作;单片机利用定时器 PWM+DMA驱动 WS2812 灯珠,通过调整 PWM 占空比生成符合时序要求的 0 码、1 码和复位信号,按 GRB 顺序控制灯光颜色,DMA 保证时序精准且不占用 CPU,最终实现环境变暗时自动点亮 WS2812 灯光的完整流程。

实验1:STM32cubeMX配置流程

引脚配置

使用到的引脚如下图(串口引脚可用来调试打印电压用,不冲突)

光敏传感器引脚:选择接入PA0(任意的可输入的引脚)配置为默认上拉(Pull-up)。

WS2812引脚:由于要使用到PWM,我们选择TIM1的PWM通道1。引脚为PA8。

PWM参数配置

PWM参数配置,基于我的时钟主频为8MHz,如图(折叠起来的使用默认参数)

分频系数:不选择分频。

计数模式:向上计数。

自动重装载值:9。

不使用影子寄存器。

计算是否符合WS2812的计数周期(1.25us):8MHz=周期为125ns,因为自动重装载值为9,则计数器需计数(9+1)次,则125ns计算10次的时间=1.25us,符合WS2812严格的时序要求。

DMA参数配置

模式为正常模式。

为TIM1_CH1添加DMA,数据搬运方向为内存到外设(对应代码中存有WS2812颜色数据的数组到TIM1_CH1通道的寄存器)。

设置外设地址不自增,内存地址自增。

设置数据长度为半字。

实验1:核心代码解读

(其余配置类代码由STM32cubeMX生成,这里不做展示,只展示核心代码)

重要的宏定义与变量

#define WS2812_NUM  16  //灯珠数量,我买的模块直接集成了16个灯珠
#define WS2812_BITS 24  //每个灯珠的24bit颜色数据位
#define TIM1_ARR 9    //PWM的ARR值,能直接修改,方便适应其他的主频

//结合主频8MHz、ARR=9,高电平持续时间 = (CCR+1) * 时钟周期。要达成0码(高电平约0.4us),需要CCR=3;1码(高电平约0.8us),需要CCR=6。
#define CODE0_HIGH 3  //根据1.25us的周期,我计算出来的0码所需要设置的高电平的占空比
#define CODE1_HIGH 6  //根据1.25us的周期,我计算出来的1码所需要设置的高电平的占空比
uint32_t WS2812_buf[WS2812_NUM*WS2812_BITS];//存取WS2812颜色数据

WS2812设置颜色函数

/**
 * @brief 设置WS2812灯带的颜色
 * @param r 红色分量 (0-255)
 * @param g 绿色分量 (0-255)  
 * @param b 蓝色分量 (0-255)
 * @retval 无
 */
void WS2812_SetColor(uint8_t r, uint8_t g, uint8_t b)
{
    // 循环计数器
    int8_t i = 0, j = 0;   
        
    // 将RGB三色合并为32位数据(格式: GRB)
    uint32_t color = ((uint32_t)g << 16) | ((uint32_t)r << 8) | (uint32_t)b; 

    // 缓冲区索引,记录当前写入位置
    uint16_t idx = 0;              
    
    // 双重循环:为每一颗LED生成对应的颜色数据
    for(j = 0; j < WS2812_NUM; j++)          // 遍历所有LED
    {
        for(i = WS2812_BITS - 1; i >= 0; i--) // 从高位到低位处理24位颜色数据
        {
            // 检查当前位是否为1
            if(color & (1 << i))               // 如果该位为1
            {
                WS2812_buf[idx++] = CODE1_HIGH; // 存储表示"1"的时序数据
            }            
            else                                // 如果该位为0
            {
                WS2812_buf[idx++] = CODE0_HIGH; // 存储表示"0"的时序数据
            }
        }
    }
}

写入颜色数组函数

/**
 * @brief 发送WS2812颜色数据到灯带
 * @note  通过TIM1的PWM+DMA方式发送数据,实现精确时序控制
 * @retval 无
 */
void WS2812_SendData(void)
{
    // 停止之前的DMA传输,确保通道空闲
    HAL_TIM_PWM_Stop_DMA(&htim1, TIM_CHANNEL_1);
    
    // 启动PWM DMA传输
    // 参数1: 定时器句柄 htim1
    // 参数2: 定时器通道 TIM_CHANNEL_1
    // 参数3: 要发送的数据缓冲区指针 WS2812_buf
    // 参数4: 数据长度 WS2812_NUM * WS2812_BITS
    // 说明: DMA会自动将缓冲区中的数据按顺序发送,每个数据决定PWM的高电平时间
    HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, 
                          (uint32_t*)WS2812_buf, 
                          WS2812_NUM * WS2812_BITS);
    
    // 延时1ms,等待DMA传输完成
    // 注意: 此延时时间需要根据实际数据量调整
    // 如果延时太短可能导致数据未发送完,太长会影响响应速度
    HAL_Delay(1);
}

main函数

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM1_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
    // 初始状态:点亮所有LED为白色(200,200,200)
    WS2812_SetColor(200, 200, 200);
    WS2812_SendData();    // 发送数据到LED灯带
	while (1)
	{
        //读取光敏传感器的信号
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET)
		{
            //低于阈值,点亮WS2812为白色(可自己调整颜色)
		    WS2812_SetColor(200,200,200);
			WS2812_SendData();
		}
		else
		{
            //高于阈值,关灯
			WS2812_SetColor(0,0,0);
			WS2812_SendData();
		}
		HAL_Delay(50);  //延时用于防止DMA在前一次传输未完成时又被启动,导致数据错乱
	}
    
    

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  
  /* USER CODE END 3 */
}

效果演示请关注dy或者b站"无敌贵点大王"

五、实验2原理(普通LED+光敏传感器)

光敏传感器+LED实现天暗自动开灯并且自调亮度

光敏传感器作用及其运作原理

使用四引脚光敏传感器,VCC与GND正常接入面包板,AO(模拟输出)接入设定IO引脚,引脚实时监测光敏传感器的模拟输出,触发相应代码,对LED进行点亮并调节亮度。

LED作用及其运作原理

具象体现ADC值对电压的影响。

一段话总结

通过光敏传感器的模拟输出(AO),持续获得因光照变化的ADC值,再通过实时变化的ADC值,实时修改PWM波的占空比,从而实现实时修改LED引脚的电压值,实现控制LED的开关和亮暗调节。

实验2:STM32cubeMX配置流程

引脚配置

使用到的引脚如下图(串口引脚可用来打印电压调试用,不冲突)

光敏传感器引脚:选择接入PA0(任意的可输入的引脚)配置为默认上拉(Pull-up)。

LED引脚:由于要使用到PWM,我们选择TIM1的PWM通道1。引脚为PA8。

PWM参数配置

PWM参数配置,基于我的时钟主频为8MHz,如图(折叠起来的使用默认参数)

分频系数:7,使得频率变成1MHz。

计数模式:向上计数。

自动重装载值:999,使得周期变成1ms。

使用影子寄存器,防止PWM波形突变

ADC参数配置(使用ADC1通道0)

独立模式;不打开扫描;不打开连续转换;转换通道数量为1。

实验2:核心代码解读

重要的宏定义和变量

/* ADC采样值范围定义 */
#define ADC_MAX  4096   /* ADC最大采样值(12位ADC,理论最大值4095,这里用4096便于计算)*/
#define ADC_MIN  1000   /* ADC最小有效采样值(用来控制触发LED点亮的阈值,值越大,点亮LED的亮度越大)*/

/* PWM比较值范围定义,便于适应不同时钟频率下的PWM值*/
#define PWM_CCR_MAX  999   /* PWM最大比较值(对应100%占空比,TIM1周期设为999)*/
#define PWM_CCR_MIN  0     /* PWM最小比较值(对应0%占空比,输出低电平)*/

获取ADC值函数

/**
 * @brief   获取ADC采样值(8次采样取平均,软件滤波)
 * @param   void
 * @return  uint32_t 返回8次采样的平均值,范围:0-4095
 * @note    此函数会阻塞运行,直到完成8次采样
 *          每次采样间隔取决于ADC转换时间(约几微秒)
 */
uint32_t Get_ADCValue()
{
    uint32_t ADC_Value = 0;  /* 最终返回的平均值 */
    uint32_t sum = 0;         /* 8次采样的累加和 */
    uint32_t count = 0;       /* 循环计数器 */
    
    /* 1. 启动ADC转换 */
    HAL_ADC_Start(&hadc1);

    /* 连续采样8次,用于软件滤波 */
    for(count = 0; count < 8; count++)
    {     
        /* 2. 等待转换完成
         * HAL_MAX_DELAY:无限等待直到转换完成
         * 函数会在转换完成后返回
         */
        HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
        
        /* 3. 读取转换结果并累加
         * HAL_ADC_GetValue() 返回12位ADC值(0-4095)
         */
        sum += HAL_ADC_GetValue(&hadc1);
        
        /* 注意:这里没有停止ADC,下次循环继续启动
         * 每次循环都 等待-读取,确保每次采样独立
         */
    }
    
    /* 4. 计算8次采样的算术平均值
     * count=8,sum是8次总和
     * 平均值 = 总和 / 8
     */
    ADC_Value = sum / count;  /* 整数除法,自动取整 */
    
    /* 5. 停止ADC,降低功耗
     * 采样完成后关闭ADC,直到下次需要采样再开启
     */
    HAL_ADC_Stop(&hadc1);
    
    /* 6. 返回滤波后的ADC值 */
    return ADC_Value;
}

计算并调整CCR的函数

/**
 * @brief   将ADC采样值映射到PWM比较值(CCR)
 * @param   ADC_Value  获取的ADC采样值(0-4095)
 * @return  uint16_t   映射后的PWM比较值(0-999)
 * @note    实现ADC值到PWM占空比的线性映射
 */
uint16_t Get_CCR(uint32_t ADC_Value)
{
    uint16_t CCR;  /* 存储计算得到的PWM比较值 */
    
    /* 映射关系:
    ADC_MIN (1000) → 0     (LED熄灭)
    ADC_MAX (4096) → 999   (LED最亮)
    */
    if(ADC_Value < ADC_MIN) ADC_Value = ADC_MIN;  /* 低于下限则取下限,最暗 */
    if(ADC_Value > ADC_MAX) ADC_Value = ADC_MAX;  /* 高于上限则取上限 ,最亮*/
    
    /* 线性映射公式:
     * 公式:CCR = 999 * (ADC - 1000) / (4096 - 1000)
     * 将[ADC_MIN, ADC_MAX]区间线性映射到[PWM_CCR_MIN, PWM_CCR_MAX]区间
     */
    CCR = (PWM_CCR_MAX * (ADC_Value - ADC_MIN) / (ADC_MAX - ADC_MIN));
    
    return CCR;  /* 返回计算得到的PWM比较值 */
}

main函数

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_TIM1_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  //提前打开PWM通道1
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  /* USER CODE END 2 */

  /* Infinite loop */
  while (1)           
  {
      //调用函数,先获取ADC值,在通过ADC值转换成CCR值
	  uint32_t ADC_Value=Get_ADCValue();
	  uint16_t duty=Get_CCR(ADC_Value);
      
      //设置转换好的CCR值,实现控灯效果
	  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty);  		  
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

效果演示请关注dy或者b站"无敌贵点大王"

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐