一、任务介绍
1、用8颗单色 LED 与 2 颗 WS2812 实现至少 4 种光效:呼吸、频闪、流水、随机闪烁。
2、4 个按键用于光效切换、速度加减、亮度加减;4 个拨码开关用于选择速度档位或占空比档位。
3、双位七段数码管显示当前光效编号与速度档位。
4、 USB 虚拟串口输出当前光效参数,便于上位机观察。
二、硬件平台
基于RP2350B设计,采用DIP40封装,支持小脚丫FPGA外设扩展板的大部分功能,板上有10颗单色LED、2颗三色彩灯、4个按键、4个开关、2个数码管以及一个姿态传感器,并有SWD调试端口,采用蛇形排孔,方便在无焊接的情况下进行扩展。
核心板:
- 8颗单色LED,可以用于各种LED相关的编程练习

- 2个三色LED,用于使用RGB三种颜色的编程练习,这里的2颗三色灯是通过一根串行信号线来控制

- 2个7段数码管,用于计数、显示数字信息,可以显示0-99之间的数字,在这里使用了两颗74HC595将3根控制信号线转换为7段数码管的驱动信号

- 4个拨码开关,用于设置、切换一些状态,其状态是使用模拟信号的方式送到RP2350B的ADC进行判断
- 4个轻触按键,用于控制信息的输入,其状态是使用模拟信号的方式送到RP2350B的ADC进行判断

三、设计方案

图1 硬件系统架构
1、输入:
按键设计:4 个按键分别映射 “光效+、光效-、亮度 +、亮度 -”。
拨码开关:映射 4 档速度(1-4 档)。
2、输出:
单色 LED:通过 PWM 驱动,占空比调节亮度,不同光效对应不同的GPIO控制方式。
WS2812 RGB 灯:通过PIO进行驱动,进而实现颜色渐变、呼吸效果。
双位数码管:采用 74HC595,减少 GPIO 占用,左位显示光效 ID(1-4 对应 4 种光效),右位显示速度档位(1-4)。
3、通信:
配置 RP2350B 的 USB 外设为虚拟串口。
四、代码介绍
使用的是C语言,软件使用VSCode,软件里面可以下载PICO的SDK,直接进行开发,非常方便。
模块划分框图
图2 底层驱动初始化模块

图3 输入处理模块

图4 光效控制模块

图5 输出模块
软件执行流程框图

图6 软件总的执行框图
1、用8颗单色 LED 与 2 颗 WS2812实现至少 4 种光效:呼吸、频闪、流水、随机闪烁。
8个LED是连接在4个IO上的,所以要想一个亮,就需要把用到的两个初始化为输出,其他的IO为高阻态,同时,为了实现后面的亮度控制,又把输出高电平的IO设为PWM输出,下面是初始化后,LED3会亮,其他LED灭。
gpio_set_dir(IO4,0);
gpio_set_dir(IO3,0);
gpio_set_dir(IO2,1);
gpio_set_dir(IO1,1);
gpio_set_pulls(IO4,0,1);
gpio_set_pulls(IO3,0,1);
gpio_put(IO2,0);
gpio_set_function(IO1,GPIO_FUNC_PWM);
slice_num[3] = pwm_gpio_to_slice_num(IO1);
pwm_set_wrap(slice_num[3], wrap);
pwm_set_clkdiv(slice_num[3], 1.0f);
pwm_set_chan_level(slice_num[3], pwm_gpio_to_channel(IO1), duty_liangdu);
pwm_set_enabled(slice_num[3], true);
换一个 LED点亮的话,需要先把之前的PWM关掉,再把想要点亮LED相关的LED初始化。
pwm_set_enabled(slice_num[3], false);
gpio_set_function(IO1, GPIO_FUNC_SIO);
led_off();
这里的led_off();函数代码如下,是因为我简单只把IO功能改回来还是会有误点亮。
void led_off(void)
{
gpio_set_dir(IO4,1);
gpio_set_dir(IO3,1);
gpio_set_dir(IO2,1);
gpio_set_dir(IO1,1);
gpio_put(IO4,0);
gpio_put(IO3,0);
gpio_put(IO2,0);
gpio_put(IO1,0);
}
想要实现呼吸灯的话,就是不断地改变PWM的占空比;频闪可以理解为LED快速地亮灭;流水效果就是按照LED的排列顺序,让LED轮流点亮;随机闪烁效果,我是使用伪随机数,随机点亮选中的LED。
WS2812是一个集控制电路与发光电路于一体的智能外控LED光源。其外型与一个5050LED灯珠相同,每个 元件即为一个像素点。像素点内部包含了智能数字接口数据锁存信号整形放大驱动电路,还包含有高精度的内部 振荡器和可编程定电流控制部分,有效保证了像素点光的颜色高度一致。 数据协议采用单线归零码的通讯方式,像素点在上电复位以后,DIN端接受从控制器传输过来的数据,首先 送过来的24bit数据被第一个像素点提取后,送到像素点内部的数据锁存器,剩余的数据经过内部整形处理电路整 形放大后通过DO端口开始转发输出给下一个级联的像素点,每经过一个像素点的传输,信号减少24bit。像素点 采用自动整形转发技术,使得该像素点的级联个数不受信号传送的限制,仅受限信号传输速度要求。
我在网上找到多种驱动WS2812的方式,有PWM+DMA,有SPI+DMA,还有PIO+DMA。这里驱动使用的是PIO+DMA。PIO代码如下:
.program ws2812
.define public T0H 3
.define public T1H 6
.define public TL 6
.wrap_target
pull block
set x, 23
bitloop:
set pins, 1
out y, 1 [T0H - 3]
jmp !y skip
nop [T1H - T0H - 1]
skip:
set pins, 0 [TL - 2]
jmp x-- bitloop
.wrap
然后就是正常PIO的初始化过程。需要注意的重点有两个,一个是分频,用下面代码:
float freq = 1.0 / 0.7e-6 * ws2812_TL;
float clk_div = clock_get_hz(clk_sys) / freq;
另一个是PIO与GPIO绑定的问题,默认情况下单个PIO只能控制32个IO,所使用核心板连接WS2812用的IO是GPIO46,所以默认是绑定不上的,所以必须使用以下函数:
pio_set_gpio_base(pio, 16);
对于RP2350来说,第二个参数可以选择0/16,0对应pins0-31,16对应32-47.必须在的创建实例和绑定函数之间使用这个函数,之后PIO与GPIO46才能正常绑定使用。
2、4 个按键用于光效切换、亮度加减;4 个拨码开关用于选择速度档位。
首先需要判断哪个键动了,硬件电路是R2R网络,根据ADC采样回来的值不一样,可以判断是哪个按键按下了。ADC的采样值需要滤波:
uint16_t adc_num(void)
{
uint16_t curr_value, max_value, min_value;
uint32_t sum;
uint8_t i;
curr_value = adc_read();
max_value = curr_value;
min_value = curr_value;
sum+=curr_value;
for(i=0;i<(ADC_AVG_NUM-1);i++)
{
curr_value = adc_read();
if(curr_value > max_value)
{
max_value = curr_value;
}
if(curr_value < min_value)
{
min_value = curr_value;
}
sum+=curr_value;
}
sum-=min_value;
sum-=max_value;
return (sum/(ADC_AVG_NUM-2));
}
经过多次判断之后才会返回采样值,然后但KEY值判断里面判断是哪个键。
对于按键,我选用两个按键负责光效切换,两个按键负责亮度加减。4个拨码开关对应4个速度挡位。光效实现就用的是前面用的LED点亮函数,只是把它们写在中断中,同时用状态机管理,延时使用进入中断的次数判断。
void key_process_for_led(uint8_t key_value)
{
if(key_last_value != key_value)
{
if((key_value>=SW0) && (key_value<=KEY3))
{
printf("key=%x \r\n", key_value);
}
key_last_value = key_value;
switch (key_value)
按一次判断一次,然后用switch 判断进入对应状态。速度挡位切换和亮度加减都在KEY值判断函数中直接运行;光效切换是给一个标志位,然后在中断中亮对应的LED亮灯效果。
case SW0:
stat_flag1=1;
waterfall_change_speed(200);
breathing_led_set_speed_direct(8);
blink_flex_set_times(50,100);
simple_random_set_speed_ms(40);
break;
case KEY3:
if (duty_liangdu >= 30 + 40)
{
duty_liangdu -= 40;
} else {
duty_liangdu = 30;
}
if (led_num >= 4)
{
led_num = 4;
}
switch (led_num)
{
case 1:stat_flag=5;break;
case 2:stat_flag=6;break;
case 3:stat_flag=7;break;
case 4:stat_flag=8;break;
default:break;
}
中断中执行:
switch (stat_flag)
{
case 5:
sendto595_1(NUM_1);
if (timer_huxi == 1)
{
led_off();
led10_huxi_init();
timer_huxi = 0;
}
breathing_led_timer_isr();
break;
3、双位七段数码管显示当前光效编号与速度档位。
重点是74HC595芯片,搞清楚使用方法。74HC595的最重要的功能就是:串行输入,并行输出。其次,74HC595里面有2个8位寄存器:移位寄存器、存储寄存器。74HC595的数据来源只有一个口,一次只能输入一个位,那么连续输入8次,就可以积攒为一个字节了。
- 74HC595的14脚:SER 英文全称是:Serial data input ,顾名思义,就是串行数据输入口。
- 74HC595的11脚:SRCLK(shift register clock input) 移位寄存器时钟引脚,上升沿有效。
- 74HC595的12脚:RCLK (storage register clock input ) 存储寄存器时钟
void sendto595_1(uint8_t num)
{
uint8_t bit_num;
uint16_t real_num = ((uint16_t) 0x02 << 8) | (uint16_t)num ;
for (char i = 0; i < 16; i++)
{
bit_num = (real_num >> 15) & 0x01;
gpio_put(SER,bit_num);
real_num = real_num << 1;
gpio_put(SCK,1);
sleep_us(1);
gpio_put(SCK,0);
}
gpio_put(RCK,1);
sleep_us(1);
gpio_put(RCK,0);
}
把第4行的0x02改为0x01就是写另一个数码管。
数码管的信息如图所示,想要什么样的数字,写对应的位就可以了。

写速度相关数码管是在低级中断里,KEY值判断之后给一个状态标志位,然后在中断里执行写数码管。光效相关的数码管在切换光效时就随着标志位变化执行了。
bool low_priority_timer_callback(repeating_timer_t *rt)
{
switch (stat_flag1)
{
case 1:
sendto595_2(NUM_1);
break;
case 2:
sendto595_2(NUM_2);
break;
case 3:
sendto595_2(NUM_3);
break;
case 4:
sendto595_2(NUM_4);
break;
default:
break;
}
4、 USB 虚拟串口输出当前光效参数,便于上位机观察。
在创建工程时,选择右边的console over USB,然后在工程中使用printf打印就可以在串口输出了。

如果在创建时没有选也不要紧,在CMakeLists.txt文件中添加如下代码即可。
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(Z1 0)
pico_enable_stdio_usb(Z1 1)
五、实物展示图


图中数据线连接VSCode串口读取打印出来的光效信息,左边的数码管展示第几个光效,右边的数码管展示第几档速度。
四个光效全部由8个LED灯实现,WS2812从上电开始执行彩色变化+呼吸。
拨码开关切换速度挡位,共四个挡位可切换,1最慢,4最快,切换同时数码管也会显示相应挡位,串口输出的速度挡位也随着变化。
上面两个按键负责切换光效,共四个光效可切换,一个光效+,一个光效-,伴随光效变化,数码管与串口输出的光效编号也随着变化。
下面两个按键负责亮度变化,共四个亮度可切换,一个亮度+,一个亮度-,可以看到LED的亮度变化,串口输出的亮度挡位也随着变化。
六、项目中遇到的难题及解决办法
1、实现了8个LED光效,不会按键检测与LED闪烁同时运行。解决办法:改造原本的LED函数,全部使用中断运行,主函数按键检测。
2、不会使用74HC595。解决方法:查资料。
3、驱动不了WS2812。解决方法:1、参考例程写PIO代码。2、RP2350B的PIO绑定GPIO46还需要多一步设置。
七、心得体会
之前少有机会写代码,更别说完成一个小成果了。经过这次完成任务的学习,对树莓派的RP2350B芯片有了更多的了解,对PICO SDK的使用更加熟练,学会使用WS2312、74HC595这样的外设,对于LED灯,R2R按键检测电路设计有了更多的了解。对自己写代码的能力也有锻炼,逐渐学习,减少生产石山代码。让自己更脚踏实地,以前总是眼高手低,觉得自己有思路就不去做了,殊不知自己的逻辑和代码编写能力都有很大的漏洞。
感谢硬禾为我们提供了这样的活动,扩展自己的知识面,希望今后活动越办越好。
