招新题流程简介(WS2812)
22物电科协软件招新题学习流程
有错误或者不当的地方请在评论区指出😘
题目简介
使用stm32驱动单一ws2812b灯珠实现呼吸灯效果,驱动及实现方法不限
演示效果
快速入门,在stm32核心板上点灯
单片机介绍
采用超大规模集成电路技术把具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、多种I/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统
入门阶段可以粗略的将其认为一个可以控制电路的小计算机
招新题所用的单片机型号为stm32f103c6t6,开发语言为c语言
前置软件
- 组合一:stm32cubeide
- 组合二:stm32cubemx+keil5
- 可选软件:flymcu
软件介绍
- 32单片机程序编写分为配置和写程序,组合二中配置过程我们选择stm32cubemx,这是一个可以图形化配置软件,比较直观,不需要自己写配置代码,软件会根据你的图形配置自己生成,写程序采用keil5,这是一个面向单片机C语言软件开发系统,集成了完善的开发环境,使用时需要破解。
- 组合一中的cubeide则集成了两个功能,具有图形化配置界面,配置生成的代码可以直接开始编写,较为方便,缺点是使用非官方的stlink烧写程序较为麻烦,下面的教程的环境均为stm32cubeide。
- flymcu为一款串口烧录软件,如果同学们选择串口烧录则可以选择下载这个软件。
- cubeide官网 https://www.st.com/zh/development-tools/stm32cubeide.html
cubemx官网 https://www.st.com/en/development-tools/stm32cubemx.html
flymcu与keil5群文件
程序烧录
这里介绍的是编写好程序后的烧写方式,可以先学习完程序的编写再学习如何烧写
烧写/烧录是指将我们在电脑上写的程序写入我们的单片机,从而让单片机可以执行我们的程序
烧写工具
- USB转TTL
- 正版stlink
- 盗版stlink
淘宝购买即可
烧写方法
- 使用正版stlink下载,写完程序并连接好单片机后点击run即可
- 使用盗版stlink下载(在keil5上可以直接使用(应该),在cubeide上如果点击run后显示需要更新固件,要先把stlink与单片机断开,然后open upgrade mode 进行更新,如果报错就refresh一下再次更新,直到下方出现进度条开始更新,更新完毕即可,然后再连接单片机,此时就可以正常下载程序了)
- 使用串口下载,首先确定代码生成的hex文件,cubeide可以点击工程属性>c/c++ build>setting>MCU Post build outputs>打钩生成hex,然后单片机需要接usb转TTL,RX接PA9,TX接PA10,5V,gnd分别与单片机5v,gnd烧录前将boot0接为1,然后使用flymcu下载,flymcu下方编程到flash时写入选项字节取消打钩,然后开始编程,烧录成功后将boot0改为0即可
开始点灯
配置程序
这一部分网上也有很多教程,这里提供的是我的配置过程,更详细的也可以参考其他教程,这里提供一个博客
- 打开软件,建立一个新的工程
选择Flie》new》stm32 project (qq截图不了这个地方,自己找一下这几个的位置) - 选择我们的单片机型号
然后点击next - 填入我们工程的名字
建议用英文,防止可能出现的路径错误,工程地址可以自己修改,也可以使用默认
然后直接点击finish即可 - 配置图形化页面
这个就是我们的单片机芯片了,是一个很直观的图形页面,可以自由拖动和配置,我们首先来配置单片机的时钟
将RCC配置为如图所示,这里是使用的外部晶振,更加稳定
将SYS配置为如图所示,便于串口调试
下面来配置IO口
将PC13 设置为GPIO_Output,即输出电平,这里就设计到我们点灯的原理了
点灯原理
在我们单片机上的led连接方法是这样的,我们知道二极管具有单向导通性,而led也就是发光二极管也具有这样的性质,因此,当led2为0v也就是接地时,led导通,灯亮,当led2为3.3V时,led两端电压一样,不会导通,灯灭。而我们的GPIO_output正是可以控制输出电压,控制的方法就是控制高低电平,我们已经学过的C/C++,二进制是由0,1构成,而在32单片机中,0就代表0v,1就代表3.3V(有误差),当我们把这个引脚设置为0时,led便会亮起。
5. 生成代码
直接点击保存或者ctrl+s即可,弹出窗口点解generation code
这里便是我们的主程序了
下面我们来尝试按照我们上面的讲解点亮这个led
6. 点灯
在main内找到while(1)循环,在前面写上
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET)
这个函数使将我们的GPIO设置为某一电平,前两个参数较容易理解,我们选的的是PC13,所以就是C13,第三个参数其实就是低电平
注意,程序一定要写在注释的begin与end之间,在这之外的都会被再次配置后的代码覆盖
下面我们烧写程序观察我们的核心板。
烧写方法在上面
可以看到灯已经亮起
7. 让灯闪烁起来
在我们的主程序中,有一个while(1){},我们学习c语言时,都是不会出现死循环的,但是在单片机中死循环是一个非常正常的事情,我们为了让程序一直循环一直我们设定的东西。
于是我们在主程序中添加如下代码
HAL_Delay();是延时程序,单位为ms,即让程序延时1s,如此我们便知道,这个程序的含义是让灯亮一秒灭一秒,并不停循环。
下面我们烧写程序观察我们的核心板
进阶——产生一个pwm波
波/信号
下面讨论的波/信号可以粗略的认为是电压的变化,这种变化可以承载着信息的传递,比如我们上面提到的二进制,比如我们想用波传递一个数字六,六在二进制下是110,那我们可以让波在两个时间单位下为高电平,一个时间单位下为低电平,这样就把信息传递了出去
pwm波的定义
PWM就是脉冲宽度调制,也就是占空比可变的脉冲波形。脉冲宽度调制是一种对模拟信号电平进行数字编码的方法。
可以简单理解为一个只由高低电平组成的周期波形,高低电平的持续时间可调
pwm波图示
根据上一节的知识,我们很容易知道ton指的就是高电平,toff就是指的低电平,而这样一个pwm波有两个参数是我们需要关注的,第一个是周期/频率,高中知识我们可以知道周期就是频率的倒数,即\(T={{1}\over{f}}\),而在图示中,我们可以知道\(T=ton+toff\),而另一个参数叫做占空比,其定义是一个周期中高电平占整个周期的时间,即\(D={{ton}\over{ton+toff}}={ton \over T}\)
尝试产生一个pwm波
在上一节的学习中,我们其实已经产生过一个pwm波,即让灯闪烁的波,根据延时的时间,我们很容易知道这个pwm波的占空比为\(50\%\),周期为\(2s\),频率为\(0.5HZ\)
下面我们将尝试产生两个不同方法下的pwm波,第一个是基于上一种方法使led有呼吸的效果,第二个是使用定时器产生更为精准的pwm波,并为招新题做好基础。
呼吸灯的定义与实现方法
原理
呼吸灯是指灯光在微电脑的控制之下完成由亮到暗的逐渐变化,感觉好像是人在呼吸。
举个不太恰当的例子,在生活中有一种可以发光的荧光棒,快速挥舞便可以看出图案,这个的原理是利用人的视觉暂留,基于人眼的分辨率不超过\(60HZ\)。而呼吸灯的实现也有类似的地方,如果我们以比较高频率的pwm波驱动led,那么led的闪烁频率就会高于我们人眼的分辨率,这样人眼看起来led就不会闪烁,是一直亮的,但是亮与亮之间也有不同。很显然,占空比90%和占空比10%的灯肯定是前者更亮一点,因为亮的时间更长一点,于是我们就可以利用这个实现呼吸灯——改变pwm的占空比。
实现
将主函数中的while(1)循环修改为
此流程介绍中均不会涉及c语言语法讲解,若对循环判断函数等有疑惑可以自行查阅课本及线上教程,这里提供一个较为全面的线上教程https://www.runoob.com/
根据上面的学习与上一节led原理,我们知道当引脚输出为低电平时led才会亮,因此占空比低时led亮,占空比高是led暗,因此循环中的两个for循环可以很容易知道分别是从暗到亮与从亮到暗,因为HAL_Delay中的参数单位是ms,因此这个pwm的周期为20ms,即50hz,这里没有超过60hz是因为HAL_Delay的分辨率不足的原因,无法实现ms级以下延时,在下面的讨论中我们会学习更为精准且分辨率更高的延时。下面是延时效果(手机拍摄效果不佳)
使用定时器产生一个pwm波
定时器介绍
定时器最基本的功能就是定时处理事情。比如定时发送USART数据、定时采集AD数据、定时检测IO口电位、还可以通过IO口输出波形等。可以实现非常丰富的功能。定时器是一个很强大的外设,不同行业使用的方式不同,知识面很广。
可以简单理解为秒表,闹钟,倒计时等等等。
利用stm32自带的pwm功能产生一个波形
定时器的使用这里暂时不会用到,只讲如何使用定时器的pwm输出产生波形,想学习的同学可以自行查阅资料配置学习,这里提供一个讲解定时器的博客
- 首先打开我们的图形化配置界面
- 修改单片机时钟频率
圈出地方本来值应该为8,直接修改为72然后点回车确定即可,这个页面配置的是单片机各个模块的工作频率,这里修改的原因是提高定时器的能力,下面也会讲这个值对定时器的影响 - 选择定时器的输出
注意选择PWM Generation,不要选择错了 - 修改定时器的预分频系数与计数周期
有关于这里数值选择与计算,这里提供一个讲解的博客
这里也简单讲一下pwm频率的计算,在第二步的时钟树频率修改,我们可以看到修改后
APB1 Timer的值为72M,这里就是我们定时器的总频率
而参数一Prescaler便是对这个值的预分频,如果将这个设为71,那么定时器的频率就会变成\(72M/(71+1)=1M\),即1MHZ,参数二Counter Period是自动重装值,即计数到多少时自动重新计数,也可以理解为计数周期,将这个地方设置为999,则定时器的频率就会变成\(1M/(999+1)=1K\),即1KHZ,这里两个值都加一是因为单片机定时器计数的方式决定的,类似于for循环中的开区间,对这里理解有困难的同学可以参考上面的博客,也可以先暂且记住,在以后的学习中逐渐理解。 - ctrl+s保存配置并更新代码
- 修改主函数
这里的函数HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
是打开pwm的输出,将Start改为Stop即为关闭,函数内的参数与之前的配置相对应。
这里的函数__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 500);
是修改pwm的占空比,初始值为0,所以这里要修改,也可以在之前配置的下方的pusle处修改,注意第三个参数是高电平的持续时间,是与计数周期相对比,因此不能大于1000,此时占空比易知\(D={500 \over 1000}*100\%=50\%\)
修改占空比的函数可以在程序内任意地方执行,因此同学们可以尝试利用这个函数实现呼吸灯 - 烧写程序,使用示波器观察
这里我们的pwm输出口在图形化配置界面中有,是PAB
有关于示波器的使用这里不会涉及,如果在宿舍想测试可以买一个小蜂鸣器或者喇叭,在下面的内容会简单说一下去年招新题的内容,就是用pwm驱动喇叭
可以看到这个pwm的占空比为50%,左上角频率为1KHZ
利用pwm波产生七个音符
我们知道声音是有频率的,而我们使用的发声器件喇叭或者无源蜂鸣器便是使用信号驱动的。如果我们用不同频率的pwm波驱动喇叭或者蜂鸣器就会发出不同的声音,下面是C调的七音符对应的频率
下面是有关这个题的一些点
- 这里占空比对声音的音调没有影响,因此默认为50%。
- 去年的招新题是利用七个按键控制喇叭发出七种音符,如何用按键控制这里暂且不会涉及,可以自行查阅GPIO_Input的使用。
- 需要注意的是,核心板带载能力有限,喇叭建议一边接电源一边接pwm输出口,或者连接功放或者三极管,或者使用带有供电端的无源蜂鸣器。
- 因为这个是去年的招新题,因此此处不做演示,有兴趣的同学可以自行制作。
- 下面提供的是小星星的简谱
进进阶,点亮N个WS2812灯珠
WS2812灯珠介绍
WS2812B是一款智能控制LED光源,控制电路和RGB芯片集成在一个5050个组件的封装中。
原理
ws2812有四个脚,一对供电脚与数据输入DIN与数据输出DOUT
控制方式为总线控制。
我们知道,颜色的表示方法有很多种,我们这里需要用到的有两种,第一种是RGB表示,第二种是HSV表示,这里先介绍RGB表示
RGB分别表示RED,GREEN,BLUE,三个的值都在0-255之间,不同的取值对应不同的颜色,类似于美术中的三原色混合,如果我们想表示一个红色,那么rgb的值就是255,0,0。
而ws2812的控制就是基于rgb值的控制,每一次传输24位数据,从前向后每8位分别是G,R,B,8位也正好是rgb的范围,因此我们想传输红色的值就是00000000 11111111 00000000.
如果我们想要驱动多个ws2812产生不同的颜色,就需要让其级联,将第一个的DOUT连接第二个的DIN,因为ws2812的数据传输原理,就像切蛋糕一样,每次经过一个就会切去一片,我们传输一个48位的数据,前24位进入第一个灯珠然后切去,后24位就会进入第二个灯珠
在单片机上的实现
原理
这里使用的方法是PWM+DMA传输,也有SPI和直接延时等方法,方法不唯一,有兴趣的同学可以自行查阅资料
首先我们要确定的是,WS2812的0与1实现方式,与之前点灯时的01不同,灯珠的01码是根据占空比来判断的
可以看到,1码的占空比高,0码的占空比低,下面是具体的参数范围
正是因为这种特性,我们才选用pwm的传输方式,下面我们来进行一个简单的计算
如果我们将预分频设为0,自动重载值设为89,那么pwm的频率就是\({72M \over {(0+1)*(89+1)}}=800K\),当我们把高电平比较值设置为61时,高电平持续时间就为\({61 \over (89+1)}*{1 \over 800K}=847us\),当我们把高电平比较值设置为28时,高电平持续时间就为\({28 \over (89+1)}*{1 \over 800K}=388us\),两个高电平持续时间正好满足1码与0码的高电平的范围,同理,可以计算出低电平的值也符合,因此,我们按照之前学习的方法控制单片机产生这两种占空比不同的pwm波,就可以传输数据
但是,这样是比较麻烦的实现,并且会有一定的延时,针对这种问题,32单片机提供了一种数据搬运的方法,DMA传输
DMA传输
介绍
DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工作。DMA 传输对于高效能 嵌入式系统算法和网络是很重要的。
这里我们只要了解如何使用与配置方法即可
配置与程序
- 首先按照上面所说配置预分频与重载值
- 按照下图配置DMA
点击ADD添加DMA,然后选择通道并如图配置即可(注意dma的方向是内存到外设,需要更改)
有关如此配置的含义,有兴趣的同学可以自行查找资料,这里提供了一个博客 - 配置工程生成单独.c/.h文件
这样配置的目的是为了我们下面代码的结构 - ctrl+s保存并更新代码
- 在工程的文件结构中添加两个文件
添加方式为右键创建一个文本文档,并将原本的文件名XX.TXT改为ws2812.c/ws2812.h(不显示后缀自行搜索如何显示后缀) - 在我们的cubeide中打开这两个文件,并写入如下代码(已经掌握原理的同学可以根据下面的讲解自行写,这里仅作为参考)
//ws2812.c
#include "ws2812.h"
unsigned char color_data[24*led_num+4];//实际GRB数据
void show()
{
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)(&color_data),sizeof(color_data));
}
void color_set(unsigned short int index,unsigned char r,unsigned char g,unsigned char b)
{
unsigned char j;
if(index >led_num)
return;
for(j = 0; j < 8; j++)
{
color_data [24 * index + j+3] = (g & (0x80 >> j)) ? bit1 : bit0; //G 将高位先发
color_data [24 * index + j + 8+3] = (r & (0x80 >> j)) ? bit1 : bit0; //R将高位先发
color_data [24 * index + j + 16+3] = (b & (0x80 >> j)) ? bit1 : bit0; //B将高位先发
}
}
void hsv_to_rgb(int h,int s,int v,float *R,float *G,float *B)
{
float C = 0,X = 0,Y = 0,Z = 0;
int i=0;
float H=(float)(h),S=(float)(s)/100.0,V=(float)(v)/100.0;
if(S == 0)
*R = *G = *B = V;
else
{
H = H/60;
i = (int)H;
C = H - i;
X = V * (1 - S);
Y = V * (1 - S*C);
Z = V * (1 - S*(1-C));
switch(i){
case 0 : *R = V; *G = Z; *B = X; break;
case 1 : *R = Y; *G = V; *B = X; break;
case 2 : *R = X; *G = V; *B = Z; break;
case 3 : *R = X; *G = Y; *B = V; break;
case 4 : *R = Z; *G = X; *B = V; break;
case 5 : *R = V; *G = X; *B = Y; break;
}
}
*R = *R *255;
*G = *G *255;
*B = *B *255;
}
//ws2812.h
#ifndef WS2812
#define WS2812
#include "tim.h"
#define bit1 61 //1码比较值为61-->850us
#define bit0 28 //0码比较值为28-->400us
#define led_num 6 //灯的数量
void color_set(unsigned short int index,unsigned char r,unsigned char g,unsigned char b);
void show(void);
void hsv_to_rgb(int h,int s,int v,float *R,float *G,float *B);
#endif
下面来解释代码内容
- 首先,创建这两个文件的目的是为了将对颜色的控制封装在一起,然后在主函数中调用,从而让整个代码结构更加简洁明了
- 然后来看.h文件,头文件tim.h是因为这里要用定时器,所以要引用这个,前两行的作用是为了重复引用。然后是宏定义部分,比较明了,是前面讲的部分。然后是一个数组,这个数组就是数据传输的数组,我们知道每个灯有24位,所以是led_num*24,而这里加4是为了让程序运行更加稳定,开始三位为0,代表reset码,清除之前的颜色,最后一位为0,代表控制颜色结束,所以是3+1=4,多了四位。下面的是函数的定义,这里的hsv_to_rgb暂且不讲,是呼吸灯用到的内容
- 最后来看.c文件,show()函数便是传输数据,用DMA的方式传输,数组中的值代表的就是pwm的比较值,比如数组中是0,0,0,61,28,61,61,28……,那么就是传输的10110……。下面的color_set()便是对颜色的修改,这里三目运算符与二进制运算的使用留给同学们自己研究,可以检验一下自己c语言学习是否清除扎实
- 修改主函数的内容
调用我们之前写的库,注意写在注释的begin与end之间
控制第一个灯为红色,注意这里传入的值0为第一个,以此类推,同时,color_set并不会使灯的颜色修改,必须要在后面加上show()才能传输 - 烧写程序并观察
这里效果可以参考文章最开始的效果,这里也附上文章开始的gif程序实现
for(int i=1;i<=6;i++){
color_set(i-1,i*50,0,255);
show ();
HAL_Delay (500);
}
for(int i=1;i<=6;i++)
color_set(i-1,255,0,255);
show ();
HAL_Delay (500);
for(int i=1;i<=6;i++)
color_set(i-1,0,255,255);
show ();
HAL_Delay (500);
for(int i=1;i<=6;i++)
color_set(i-1,255,255,0);
show ();
HAL_Delay (500);
呼吸灯的实现
原理
根据之前的学习,我们有一种方式使灯珠产生呼吸的效果,即不停地对灯珠进行点亮与reset,控制明暗产生呼吸的效果,同学们可以自行尝试这种方法。下面介绍的是另一个较为简单的方法,利用颜色的HSV值
HSV
HSV(Hue, Saturation, Value)是根据颜色的直观特性由 A. R. Smith 在 1978 年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。这个模型中颜色的参数分别是色调(H)、饱和度(S)和明度(V)。
因此,我们可以通过控制V的值来实现颜色的明暗变化,这里也很好理解,颜色本来就有明暗之分,比如亮红与暗红。
HSV与RGB的转换
下面提供的是HSV向RGB的转换公式
根据转换公式,我们便得到了之前提到的hsv_to_rgb函数
参数的范围h(0~360),s(0~100),v(0~100)
修改主函数实现呼吸灯
这里HAL_Delay的目的是降低呼吸频率,可以计算得到这个程序的呼吸时间为1.2s
烧写程序观察
这里也提供了一个常见颜色对照表,同学们可以选择喜欢的颜色实现呼吸灯的效果
结语
本次招新题的个人建议流程到这里也就结束了,主要是讲了stm32的配置与pwm波的产生,具体实现细节还需要同学们实践学习,有疑问的地方也欢迎提问。对于本题的发挥部分这里不做讲解,同学们可以自行查阅资料学习,培养好的自学能力是大学生活很重要的一环,这里也提供几个我大一学习时参考的博客。
串口收发
OLED配置
ADC采集