题目
使用STM32单片机制作一个应援棒(点亮一个LED),使其可以通过按键切换不同的发光样式。
基础要求
1.使用STM32单片机,自行编写程序
2.自行焊接外设:WS2812(或者使用板载LED)
3.通过按键可以控制多个发光样式的切换,按键按一次切换一次发光样式。
注:本题只要求使用一个LED灯,发光样式包括:常亮,常灭,低频闪烁,高频闪烁,呼吸灯效果等等。默认开机第一个发光样式为常灭。
效果示例
STM32快速入门
单片机简介
单片机是采用超大规模集成电路技术把具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、多种I/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统。
简单来讲,单片机就是一个可以控制电路的小型计算机。
本次招新题使用的单片机型号是STM32F103C8T6,开发语言为C语言。
软件要求
可用软件组合
组合一:stm32cubeide
组合二:stm32cubemx+keil5
可选软件:flymcu
软件介绍
32单片机程序编写分为配置和写程序两部分。组合二中配置过程我们选择stm32cubemx,这是一个图形化配置软件,比较直观。不需要自己写配置代码,软件会根据你的图形配置自己生成。写程序采用keil5,这是一个面向单片机C语言软件开发系统,集成了完善的开发环境,使用时需要破解。
组合一中的cubeide则集成了两个功能,具有图形化配置界面,配置生成的代码可以直接开始编写,较为方便,缺点是使用非官方的stlink烧写程序较为麻烦,下面的教程的环境均为stm32cubeide。
flymcu为一款串口烧录软件,如果同学们选择串口烧录则需要下载这个软件。
cubeide、flymcu在群文件中。Keil5可自行百度资源。
程序烧录
( 这里介绍的是编写好程序后的烧写方式,可以先学习完程序的编写再学习如何烧写。)
烧写/烧录是指将我们在电脑上写的程序写入我们的单片机,让单片机可以执行我们的程序。
烧写工具
USB转TTL
正版stlink
盗版stlink
以上工具大家可以自行去淘宝购买
烧写方法
使用正版stlink下载,写完程序并连接好单片机后点击run即可
使用盗版stlink下载(在keil5上可以直接使用(应该),在cubeide上需要按照http://t.zoukankan.com/DragonStart-p-12199455.html才能识别到盗版stlink)
使用串口下载,首先确定代码生成的hex文件,cubeide可以点击工程属性>c/c++ build>setting>MCU Post build outputs>打钩生成hex。单片机需要用usb转TTL,RX接PA9,TX接PA10。5V,gnd分别与单片机5v,gnd相连。烧录前将boot0接为1,按下RESET按钮。最后使用flymcu下载,flymcu下方编程到flash时写入选项字节取消打钩,然后开始编程,烧录成功后将boot0改为0,再按下RESET按钮。
此时BOOT0为1状态
此时BOOT为0状态
STM32CubeIDE的使用
初次配置单片机
这里提供的是我的配置过程,更详细的也可以参考其他教程,这里提供一个博客
1、打开软件,建立新工程
2、选择单片机型号为F103C8T6
3、给工程起名。建议使英文名字,避免中文导致路径报错。工程地址可以修改也可以默认,但是要留意自己的工程存储位置
4、配置单片机
展开刚才创建的新项目,双击.ioc文件,进入配置界面
这个就是我们的单片机芯片了,是一个很直观的图形页面,可以自由拖动和配置。接下来先进行单片机时钟的配置:
将RCC配置为如图所示,这里是使用的外部晶振,更加稳定。
将SYS配置为如图所示,便于串口调试。接下来配置IO口。
将PC12设置为GPIO_Output,即输出电平。这里就涉及到我们点灯的原理了。注意这次发的板子上的板载led是连在pb12上的,与图中的PC13有出入。
5、生成代码,直接点击保存或Ctrl+S即可。
第一次阅读的同学,请跳转到原理介绍与程序编写,参考板载LED的点亮方法后再学习下面的部分。
再次配置单片机
PWM波介绍
PWM(Pulse Width Modulation),中文名脉冲宽度调制。简称脉宽调制,通俗的讲就是调节脉冲的宽度,是电子电力应用中非常重要的一种控制技术。PWM中我们关心的参数有脉宽,周期盒占空比。
图中Ton表示高电平(电压高)Toff表示低电平(电压低)
由于WS2812的0与1实现方式是根据占空比来判断的(相关参数见下),因此,我们需要让单片机输出不同占空比的pwm波,实现对WS2812的控制。
配置单片机产生PWM波
1、回到.ioc文件,选择Clock Configuration,将红圈处改成72并回车。
2、按照红圈中提示设置PWM Channel,预分频和重载数
预分频和重载的数值选择与计算可以参考这个博客。
3、按下Ctrl+S配置,更新代码。可以从图形化界面中看到,我们的PWM输出口是PA8。这也是稍后WS2812中DI线连接的脚。
4、相关函数讲解
在while前加上以下两行代码,即可产生PWM波。可用示波器连接在PA8上验证。
PWM输出口的显示
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 500);
第一行的HAL_TIM_PWM_Start是启动PWM的函数,括号内分别是启动的定时器和Channel。第二行__HAL_TIM_SET_COMPARE则是产生PWM波的函数。括号内前两个参数分别指定定时器和Channel,第三个参数500是调整占空比的参数,在0-1000之内取值,占空比是这个参数的千分之一。
配置DMA
DMA(Direct Memory Access,直接存储器访问) 允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。这里我们只要了解如何使用与配置方法。
在定时器设置中找到DMA Settings的选项卡,打开CH1的DMA。注意方向是内存到外设。有关配置的含义,这里提供一个博客。
最后,为了方便后续添加驱动,在Project Manager下找到配置工程生成单独.c/.h文件,打上勾。
所有配置工作完成后记得按下Ctrl+S保存配置并生成代码,并请继续学习WS2812的点亮。
配置开关
1、选择一个引脚(以PA3)为例,将脚设置为GPIO_Input。
2、在.ioc中选择GPIO,将脚设置为Pull-up(上拉)。
原理介绍与程序编写
板载LED的点亮
点灯原理
单片机上的LED连接情况
当led2为0v,也就是接地时,led导通,灯亮,当led2为3.3V时,led两端电压一样,不会导通,灯灭。而我们的GPIO_output正是可以控制输出电压,控制的方法就是控制高低电平。在32单片机中,0就代表0v,1就代表3.3V(有误差),当我们把这个引脚设置为0时,led便会亮起。
点灯代码
展开工程下的Core文件夹,可以看到Inc和Src两个文件夹。Inc文件夹内存储的是我们的头文件(.h=header),这部分对我们的正式代码起一个描述作用。Src文件夹存储可运行的代码。其中我们一般只需要修改main.c即可,剩下的文件可以自动生成。
在main.c中找到while循环。在前面写上
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_12,GPIO_PIN_RESET);
这个函数的用处是:将我们的GPIO设置为某一电平。前两个参数较容易理解,我们选的的是PC12,所以就是C12。第三个参数是低电平的意思,高电平是GPIO_PIN_SET。
注意代码只能写在/USER CODE BEGIN/和/USER CODE END/之间,不然重新配置.ioc文件后会被删除!
写好保存后进行编译,烧录进单片机,效果如下:
让灯闪烁
在while内部添加如下代码
注意单片机中可能要求重复执行某些命令(比如本例子中灯的闪烁),因此允许死循环的存在。HAL_Delay();是延时程序,单位为ms,即让程序延时1s,如此我们便知道,这个程序的含义是让灯亮一秒灭一秒,并不停循环。
最终效果如下所示:
完成这一部分后,请跳转到再次配置单片机的部分,学习配置PWM和DMA。
WS2812的点亮
驱动添加
按要求配置好后,分别在Inc和Src文件夹下新建ws2812.h/ws2812.c文件,如图所示:
将以下内容复制到ws2812.h中:
//ws2812.h
#ifndef WS2812
#define WS2812
#include "tim.h"
#define bit1 61 //1码比较值为61-->850us
#define bit0 28 //0码比较值为28-->400us
#define led_num 8 //灯的数量
void color_set(unsigned short int index,unsigned char r,unsigned char g,unsigned char b);
void show(void);
#endif
将以下内容复制到ws2812.c中:
//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将高位先发
}
}
下面是对代码的解释:
1、这两个文件将对WS2812的颜色控制封装在一起,这样在主函数中调用的时候更加简洁。
2、.h文件中,前两行是条件编译,可以避免重复定义。头文件tim.h是对定时器的引用。几个数据的宏定义可以根据高低位的占空比计算得到。最后的数组是传输数据用:我们知道每个灯有24位,所以是led_num*24,而这里加4是为了让程序运行更加稳定,开始三位为0,代表reset码,清除之前的颜色,最后一位为0,代表控制颜色结束,所以是3+1=4,多了四位。下面的是函数的声明,会在.c中给出定义。
3、.c文件中,show函数就是以DMA方式传输数据,数组中的值就是PWM的比较值。color_set函数是修改颜色的函数。这里用到了三目运算符和移位运算符,可以参考这个页面。
驱动调用
最后,我们还要在main.c的前面加上
#include"ws2812.h"
这样就可以调用color_set和show函数了。注意在color_set之后必须跟着show函数,否则数据无法传输到灯上。
最后附上示例代码:
switch (flag2) {
case 0:
for (int j = 0; j < 8; j++){
color_set(j, 0, 0, 0);
}
show();
break;
case 1:
for (int j = 0; j < 8; j++){
color_set(j, 0, 0, 255);
}
show();
break;
case 2:
for (int j = 1; j <= 8; j++) {
color_set(j - 1, 255 ,255 , 0);
show();
HAL_Delay(100);
}
HAL_Delay(500);
for (int j = 7; j >= 0; j=j-1) {
color_set(j, 0, 0 , 0);
show();
HAL_Delay(100);
}
break;
case 3:
for (int j = 0; j < 8; j++)
color_set(j, 0, 255, 255);
show();
HAL_Delay(300);
for (int j = 0; j < 8; j++)
color_set(j, 0, 0, 0);
show();
HAL_Delay(300);
break;
}
开关的使用
开关与GPIO
GPIO=General Purpose Input Output,通用输入输出。其最常见的应用就是开关。GPIO的输入模式有:浮空输入、上拉输入、下拉输入、模拟输入;输出模式有:开漏输出、开漏复用输出、推挽输出、推挽复用输出。招新题中涉及到的只有上拉输入和下拉输入。
下拉电阻:把信号通过电阻连接到低电平。信号初始化为低电平。上拉电阻:把信号通过电阻连接到高电平。信号初始化为高电平。
开关消抖
按照前面的步骤配置完开关后,要想使用开关还需要进行消抖。通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。为确保CPU对键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到键释放稳定后再作处理。
方法1:软件消抖。在检测到按键按下时,加个延时函数,然后再检测一次。延时函数往往不用太长(10ms即可),因为抖动的时间也很短。
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)==0)
{
HAL_Delay(10); //延时10ms
//再次检测按键是否按下
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)==0)
{
...
}
}
方法2:硬件消抖。在按键两端并联一个电容即可,由于电容两端的电压不能突变,会消除按键按下带来的抖动。
消抖的方法还有很多,这里可以参考这个博客。
最终程序示例
while (1) {
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == 0) {
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == 0) {
flag = (flag + 1) % 4;
switch (flag) {
case 0:
OLED_operate_gram(0);
OLED_show_string(24, 16, "Hello World!");
flag2 = 0;
break;
case 1:
OLED_operate_gram(0);
OLED_show_string(24, 16, "HAPPY!");
flag2 = 1;
break;
case 2:
OLED_operate_gram(0);
OLED_show_string(24, 16, "Stand Up!");
flag2 = 2;
break;
case 3:
OLED_operate_gram(0);
OLED_show_string(24, 16, "Last");
flag2 = 3;
break;
default:
break;
}
OLED_refresh_gram(); //更新显示到OLED
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
switch (flag2) {
case 0:
for (int j = 0; j < 8; j++){
color_set(j, 0, 0, 0);
}
show();
break;
case 1:
for (int j = 0; j < 8; j++){
color_set(j, 0, 0, 255);
}
show();
break;
case 2:
for (int j = 1; j <= 8; j++) {
color_set(j - 1, 255 ,255 , 0);
show();
HAL_Delay(100);
}
HAL_Delay(500);
for (int j = 7; j >= 0; j=j-1) {
color_set(j, 0, 0 , 0);
show();
HAL_Delay(100);
}
break;
case 3:
for (int j = 0; j < 8; j++)
color_set(j, 0, 255, 255);
show();
HAL_Delay(300);
for (int j = 0; j < 8; j++)
color_set(j, 0, 0, 0);
show();
HAL_Delay(300);
break;
}
}
示例代码中有部分代码和OLED有关,有兴趣的同学可以尝试。驱动可以在这里找到。
祝大家能顺利完成招新题,学有所得!