蓝桥杯嵌入式备赛手册

时间:2022-05-06 18:53:30

本文是我参加蓝桥杯嵌入式比赛后的一些心得体会和一些自己总结的驱动代码,希望能给以后参加蓝桥杯嵌入式的同学带来一些帮助。

本文没有经过校对,如有错误还请包涵,欢迎大家交流和指正,转载请注明出处。

一、 总述

首先说一下自己的情况:

我参加的是第九届蓝桥杯嵌入式比赛

省赛备赛两周(平均每天花费4——6小时),最后省一进入国赛

国赛备赛一周半(平均每天花费4——6小时),最后国二

我是计算机科学与技术专业的学生,对模拟电路知识有所欠缺,所以客观题模拟电路部分只能看脸。

之前有过使用stm32做小项目的经历可以快速上手(看到这里不要急,拿不拿奖与有没有stm32使用经历没有太大关系)

再谈谈对蓝桥杯嵌入式比赛的认识:

简介套话不说,这个比赛我个人认为得奖很容易,但想拿国一及以上就有些难度了,需要一定的训练加一定的运气。国二以上基本都是在客观题上分高低。

比赛考察范围有限且基本上都是套路,有限的知识和套路全部掌握了就能拿到国二,我也会在接下来的文章里把所有要掌握的点全部给大家梳理清楚

这个比赛主要针对的是电子工程类专业学生和计科学生,如果不是这些专业的学生就需要补充一些基础知识(C语言、微机原理、数电模电…)

比赛还是主要面向双非学校学生,主要考察对stm32的使用(毕竟占总成绩70%的客观题就是写个小程序),其次在客观题会考察一些stm32、Cortex-M3、嵌入式、数电模电的一些基础知识

我将会总结什么:

  • 如何构建工程
  • 我认为必须掌握的驱动及其运用
  • 怎么通过比赛提供的资料迅速完成我们的程序
  • 我对考察点的个人心得,以及怎么分配我们的备赛时间
  • 客观题的准备

PS:

这个比赛最大的优点也是最大的缺点便是:考察范围有限

看完这篇文章并且掌握我所提到的东西,至少能拿省一。欢迎交流与分享,切勿商用哦。

二、比赛提供

所有童鞋一定会问到比赛时提供什么,这一点官网也没有做很好的说明,在这里我说明一下:

2.1 省赛提供

2.1.1 省赛硬件方面

第八届和第九届的省赛都是只提供竞赛板,不涉及扩展板内容

2.1.2 省赛软件方面

省赛提供资料图:

蓝桥杯嵌入式备赛手册

  • CT117E(就是竞赛板)电路原理图和竞赛板使用说明

    比赛时如果忘记板上资源对应的引脚,电路原理图用来查看对应引脚。
    
  • 串口调试工具

    如果考察到串口(UART),需要用调试工具接收和发送来验证程序
    
  • 驱动及插件

    FT2232驱动和CooCox驱动,这些都用不到,不要管它
    
  • 数据手册

数据手册资料图:

蓝桥杯嵌入式备赛手册

这些数据手册各有各的用处,我认为最有用的是stm32f103rbt6.pdf,怎么看数据手册以及什么时候看后面会讲。
- 液晶驱动参考例程

液晶驱动参考历程资料图:

蓝桥杯嵌入式备赛手册

一个已经写好的工程CT117E-LCD: 这个工程可以作为我们进考场时测试板子及其屏幕是否正常工作,
    也可以直接在这个工程上构建我们的程序,省略了我们重新建工程的时间(我选择重新构建工程)

lcd驱动文件:一个.c源文件和两个.h头文件,这三个就是lcd的驱动文件,后面会讲到如何使用它们

- i2c参考程序

    只有i2c.c和i2c.h两个文件,只在E2PROM和陀螺仪的时候能用到,后面会讲如何使用

- STM32固件库v3.5

固件库资料图:

蓝桥杯嵌入式备赛手册

这个固件库十分关键,一方面要用来构建工程,一方面又能利用库函数和里面的Example快速编写驱动

2.2 国赛提供

在硬件上:竞赛板+扩展板

国赛在省赛提供的所有资料的基础上,增加了:

  • 扩展板的相关数据手册和电路原理图
  • DS18B20和DHT11的驱动

    只有.c和.h文件,没有工程和demo
    

三、构建工程

万里长城第一步,先把stm32工程给建立起来,构建工程需要用到比赛提供的STM32固件库和keil

keil的安装请参照网上的各种教程,多的数不清~

3.1 建议使用keil4

这里需要特殊说明一下,比赛时提供的是keil4,而如果你平常训练使用keil5的话主要有两点不同:
1. keil4没有代码自动联想补全功能,所以如果你使用keil5进行平常联系,我建议你关掉代码联想功能。
2. keil4在设置那个Colink下程序进板与keil5有些许的差别,我下面在构建工程时会讲到如何设置。

综上所述,我建议在平常联系就跟比赛时一样采用keil4,避免一些不必要的麻烦。

3.2 构建工程

首先在先在一个目录下(磁盘上的任何地方)建立一个文件夹

可以看到我在C盘目录下建立了一个名叫test的文件夹

蓝桥杯嵌入式备赛手册

之后在test文件夹下建五个文件夹

蓝桥杯嵌入式备赛手册

  • CORE: 用来存放内核文件
  • FWLIB:用来存放固件库的源文件和头文件
  • HARDWARE:用来存放驱动程序代码
  • OUTPUT: 用来存放在编译过程中产生的中间文件
  • USER: 用来放用户文件和main函数文件,以及把工程项目建立在此处

接下来便是往这五个文件夹里copy东西了,打开比赛提供的STM32固件库V3.5

可以看到在文件夹“STM32F10x_StdPeriph_Lib_V3.5.0”的内容有:

蓝桥杯嵌入式备赛手册

给CORE文件夹里添加文件

  • 将Libraries->CMSIS->CM3->CoreSupport文件夹下的“core_cm3.c和core_cm3.h”文件放入CORE文件夹内
  • 将Libraries->CMSIS->CM3->CoreSupport->ST->STM32F10x->startup->arm文件夹下的”startup_stm32f10x_md.s”放入文件夹内

最终CORE文件夹内的文件有:

蓝桥杯嵌入式备赛手册

给FWLIB文件夹里添加文件

  • 将Libraries->STM32F10x_StdPeriph_Driver文件夹下的“inc文件夹和src文件夹”一起放入FWLIB文件夹内

最终FWLIB文件夹内的文件有:

蓝桥杯嵌入式备赛手册

给USER文件夹里添加文件

  • 将Libraries->CMSIS->CM3->CoreSupport->ST->STM32F10x文件夹下的“stm32f10x.h、system_stm32f10x.c和system_stm32f10x.h”放入USER文件夹内
  • 将Project->STM32F10x_StdPeriph_Template文件夹下的“main.c、stm32f10x_conf.h、stm32f10x_it.c.c和stm32f10x_it.h”放入USER文件夹内

最终USER文件夹内的文件有:

蓝桥杯嵌入式备赛手册

打开keil4,新建工程

点击keil4中Project->New uVision Project:

蓝桥杯嵌入式备赛手册

将工程建立在我们的test->USER文件夹下,命名为test:

蓝桥杯嵌入式备赛手册

芯片选择为STM32F103RB:

蓝桥杯嵌入式备赛手册

添加工程文件

点击这个组建来修改和添加目录和文件:

蓝桥杯嵌入式备赛手册

将Project Targets修改为”test”,test下增加4个Groups分别为”CORE”、”FWLIB”、”USER”、”HARDWARE”:

蓝桥杯嵌入式备赛手册

点击Add Files,把CORE文件夹下的core_cm3.c和startup_stm32f10x_md.s添加到CORE:

蓝桥杯嵌入式备赛手册

把USER文件夹下的 main.c、stm32f10x_it.c和system_stm32f10x.c添加到USER:

蓝桥杯嵌入式备赛手册

FWLIB里面添加的是需要用到的库函数源文件,可以全部添加也可以只添加要用到的。我在这里把FWLIB->src文件夹下所有库函数源文件都添加进来(其实没有必要,但也仅仅是编译时慢了些):

蓝桥杯嵌入式备赛手册

此时,可以看到keil4里左侧的项目目录已经发生了变化

蓝桥杯嵌入式备赛手册

打开main.c把里面所有文件清空

写下以下代码:

int main()
{
    while(1)
    {

    }
}

写过嵌入式代码的童鞋应该对这两行代码并不陌生,main.c 文件最终样式:

蓝桥杯嵌入式备赛手册

对工程进行相关设置

点击Target Options…,进入工程设置的组件:

蓝桥杯嵌入式备赛手册

在Output中点击”Select Folder for Objects..”:

蓝桥杯嵌入式备赛手册

选择OUTPUT文件夹:

蓝桥杯嵌入式备赛手册

在C/C++中的Define里填写

USE_STDPERIPH_DRIVER,STM32F10X_MD
  • 这一句话可以背过也可以在stm32f10x.h的95行找到:

蓝桥杯嵌入式备赛手册

填写后效果为:

蓝桥杯嵌入式备赛手册

随后点击Include Path的选项增加头文件路径,分别添加CORE、FWLIB->src、HARDWARE、USER:

蓝桥杯嵌入式备赛手册

在Debug中点击右边的Use并选择CooCox Debugger:

蓝桥杯嵌入式备赛手册

点击Settings,并在Debug选项里选择Adapter为Colink:

蓝桥杯嵌入式备赛手册

再在Flash Download里点击Add,添加“STM32F10X Med-density Flash”:

蓝桥杯嵌入式备赛手册

最后一步也是keil4和keil5有区别的地方,keil4需要在Utilities中选择CooCox Debugger(这一步keil5不需要):

蓝桥杯嵌入式备赛手册

设置完后,编译程序并下板子

这下全部都设置完了,点击OK后,点击Build会得到 “0 Error(s), 0 Warning(s)”:

蓝桥杯嵌入式备赛手册

将生成的可执行文件下到板子里点击Download组件(如果下板子失败,把工程关闭重新打开,keil4有bug):

蓝桥杯嵌入式备赛手册

至此一个工程已经全部构建完毕

3.3 对于构建工程的个人建议

建议自己构建工程

你可以选择直接使用比赛时提供的液晶驱动例程,也可以选择自己构建工程。

我建议自己构建工程,个人认为液晶驱动例程的结构不清晰,自己构建的用着顺手。

驱动文件编写和存放

我把所有驱动文件都存在HARDWARE文件夹下。

每次新建一个文件,保存为.c或者.h文件都存在HARDWARE文件夹里,并在main.c中调用。

比如写LED的驱动,点击New就新建了一个文件,系统自动命名为”Text1”:

蓝桥杯嵌入式备赛手册

接下来点击Save,将文件命名为”led.c”并保存到HARDWARE:

蓝桥杯嵌入式备赛手册

同样的再创建一个新文件,命名为”led.h”并保存到HARDWARE,双击左侧Project里的HARDWARE将”led.c”添加进工程:

蓝桥杯嵌入式备赛手册

添加完后可以看到左侧Project里的HARDWARE目录下出现了”led.c”:

蓝桥杯嵌入式备赛手册

接下来便是编写”led.c”和”led.h”代码,之后在main.c里include “led.h”来使用。具体的驱动代码在后面会讲。

建议经常练习建工程

我建议每次练习一个题目都重新构建一遍工程,毕竟孰能生巧,在比赛时最好不要在准备工作上出问题。

四、快速编写必备驱动

4.1 LED

LED驱动其实就是单纯对GPIO进行操作。

但是这里加入了一个74HC573作为数据锁存器,所以每次操作需要给PD一个下降沿。

4.1.1 LED硬件连接

蓝桥杯嵌入式备赛手册

LED STM32
LED0 ~ 7 PC8 ~1 5
LE PD2

4.1.2 LED软件驱动

led.c:

#include "led.h"

void led_Init()
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = LED_ALL;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
}

void led_Control(u16 led, u8 state)
{
    if(state==1)
    {
        GPIO_ResetBits(GPIOC,led);
        GPIO_SetBits(GPIOD,GPIO_Pin_2);
        GPIO_ResetBits(GPIOD,GPIO_Pin_2);
    }
    else if(state==0)
    {
        GPIO_SetBits(GPIOC,led);
        GPIO_SetBits(GPIOD,GPIO_Pin_2);
        GPIO_ResetBits(GPIOD,GPIO_Pin_2);
    }
}

led.h:

#ifndef __LED_H
#define __LED_H

#include "stm32f10x.h"


#define LED_ALL GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15

#define LED1 GPIO_Pin_8
#define LED2 GPIO_Pin_9
#define LED3 GPIO_Pin_10
#define LED4 GPIO_Pin_11
#define LED5 GPIO_Pin_12
#define LED6 GPIO_Pin_13
#define LED7 GPIO_Pin_14
#define LED8 GPIO_Pin_15

void led_Init();
void led_Control(u16 led, u8 state);

#endif

4.1.3 LED驱动使用

在main.c文件中包含led.h头文件:

#include "led.h"

在main函数初始化阶段直接调用LED初始化函数初始化LED

int main()
{
    ...
    led_Init();
    ...
    while(1)
    {
        ...
    }
}

如果想点亮LED1则直接调用:

led_Control(LED1,1);

如果想让LED1~8全灭则调用:

led_Control(LED_ALL,0);

4.1.4 利用库函数的Example快速写LED驱动

LED的初始化驱动代码其实就是初始化了GPIO,设置对应引脚为推挽输出

LED的控制驱动代码就是对GPIO进行高低电平的输出

全是调用库函数来实现的,我们可以不必完全背过每个库函数的具体写法,直接从相应的库的Example里复制粘贴即可,大大减少比赛时间。

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->GPIO->IOToggle”文件夹下,打开”main.c”

可以看到里面有基本上所有LED初始化驱动所需的代码,直接复制,改改对应引脚和对应使能时钟即可:

蓝桥杯嵌入式备赛手册

  • 增加使能时钟GPIOC
  • 增加设置GPIO_Pin_8 ~ GPIO_Pin_15为推挽输出

设置引脚高低电平的函数可以在keil左侧的Project里找到”stm32f10x_gpio.h”,打开后拉到文件最底找到:

蓝桥杯嵌入式备赛手册

如果快速找到这些代码,就可以大大节省敲代码所花费的时间,把更多的时间留在逻辑层,增加得奖的可能性

我个人的原则就是,能复制粘贴解决的事就不自己敲,因为这是比赛,有时间限制,要让自己得奖可能最大化

4.2 SysTick来实现延时函数

为什么要先说延时函数呢?本来是要说按键驱动的,而按键我使用外部中断,利用延时函数来消抖,还是先说一下延时函数。

4.2.1 毫秒延时函数

stm32f10x_it.c:

u32 delaytime;
void delay_ms(u32 time)
{
    delaytime=time;
    while(delaytime!=0);
}
...
void SysTick_Handler(void)
{
    delaytime--;
}

直接在stm32f10x_it.c文件中写上述代码,SysTick_Handler()的空函数在stm32f10x_it.c文件底部:

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

4.2.2 更改SysTick_Config

因为之后要用到我们的延时函数来打断外部中断对按键进行消抖,所以要更改SysTick_Config的中断优先级。

然而SysTick_Config()函数在内核文件”core_cm3.h”中:

蓝桥杯嵌入式备赛手册

比赛规定内核文件是不允许被修改的,我们直接把这个函数复制出来放到main.c中,重新命名为”SysTick_Config1()”

将这一行代码:

 NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);

修改为:

NVIC_SetPriority (SysTick_IRQn, 1);

复制粘贴和修改后main.c里的SysTick_Config1():

蓝桥杯嵌入式备赛手册

4.2.3 使用delay_ms()来延时

先在main()函数中进行相关初始化,这里连带着把中断分组也一起设置了:

int main()
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SysTick_Config1(SystemCoreClock/1000);
    ...
    while(1)
    {
        ...
    }
}

如果想在某处延时500毫秒,直接调用:

delay_ms(500);

这里说一下,NVIC_PriorityGroupConfig()可以在”misc.h”里找到,SystemCoreClock可以在”system_stm32f10x.c”里找到

还是那句话能复制粘贴就复制粘贴,省时间还减少出错率

4.3 KEY(基础板上的四个按键)

这四个按键有两个方法进行驱动
- 循环扫描
- 外部中断

我个人建议使用外部中断,因为我们的嵌入式系统还要干其它工作,尤其如果要捕获PWM,刷新屏幕或者有时间间隔很小的中断任务要执行,循环扫描就不那么好使了。

但也不是绝对的,还是要根据具体需求具体分析,但是使用外部中断来作为按键驱动目前来看还没发现什么缺点。

所以我强烈建议使用外部中断,并且我下面也会讲怎么用比赛提供的库函数Example简化我们写驱动的任务。

4.3.1 KEY硬件连接

蓝桥杯嵌入式备赛手册

KEY STM32
KEY1 PA0
KEY2 PA8
KEY3 PB1
KEY4 PB2

4.3.2 KEY软件驱动

key.c:

#include "key.h"

void key_Init()
{
    EXTI_InitTypeDef   EXTI_InitStructure;
    GPIO_InitTypeDef   GPIO_InitStructure;
    NVIC_InitTypeDef   NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; 
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);


    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource8);
    EXTI_InitStructure.EXTI_Line = EXTI_Line8;
    EXTI_Init(&EXTI_InitStructure);
    NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
    NVIC_Init(&NVIC_InitStructure);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
    EXTI_InitStructure.EXTI_Line = EXTI_Line1;
    EXTI_Init(&EXTI_InitStructure);
    NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
    NVIC_Init(&NVIC_InitStructure);

    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource2);
    EXTI_InitStructure.EXTI_Line = EXTI_Line2;
    EXTI_Init(&EXTI_InitStructure);
    NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
- [ ]   NVIC_Init(&NVIC_InitStructure);
}

void EXTI0_IRQHandler(void)
{
    delay_ms(5);
  if(EXTI_GetITStatus(EXTI_Line0) != RESET)
  {

    EXTI_ClearITPendingBit(EXTI_Line0);
  }
}

void EXTI9_5_IRQHandler(void)
{
    delay_ms(5);
  if(EXTI_GetITStatus(EXTI_Line8) != RESET)
  {

    EXTI_ClearITPendingBit(EXTI_Line8);
  }
}

void EXTI1_IRQHandler(void)
{
    delay_ms(5);
  if(EXTI_GetITStatus(EXTI_Line1) != RESET)
  {

    EXTI_ClearITPendingBit(EXTI_Line1);
  }
}

void EXTI2_IRQHandler(void)
{
    delay_ms(5);
  if(EXTI_GetITStatus(EXTI_Line2) != RESET)
  {

    EXTI_ClearITPendingBit(EXTI_Line2);
  }
}

key.h:

#ifndef __KEY_H
#define __KEY_H


#include "stm32f10x.h"

key_Init();
#endif

4.3.3 KEY驱动的使用

在main()函数中调用key_Init()完成KEY驱动初始化:

int main()
{
    ...
    key_Init();
    ...
    while(1)
    {
        ...
    }
}

增加按键功能则只需在对应外部中断响应函数中添加代码即可,比如按下KEY1点亮LED1:

void EXTI0_IRQHandler(void)
{
    delay_ms(5);
  if(EXTI_GetITStatus(EXTI_Line0) != RESET)
  {

    led_Control(LED1,1);

    EXTI_ClearITPendingBit(EXTI_Line0);
  }
}

4.3.4 利用库函数的Example快速写KEY驱动

跟LED驱动一样,KEY驱动也可以在对应的库函数Example中找到大量我们需要的代码,减少写驱动的时间。

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->EXTI->EXTI_Config”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

再打开”EXTI_Config”下的”stm32f10x_it.c”:

蓝桥杯嵌入式备赛手册

有大量可以复制粘贴的代码,修改和增加我们需要的部分即可。

4.4 LCD驱动的使用

直接把比赛提供的LCD驱动复制粘贴到我们的HARDWARE,添加进工程

一共是三个文件:
- lcd.c
- lcd.h
- fonts.H

4.4.1 初始化LCD

在main函数中调用:

int main()
{
    STM3210B_LCD_Init();
    delay_ms(500);
    LCD_Clear(White);
    LCD_SetTextColor(Blue);
    LCD_SetBackColor(White);
}
  • 记得在STM3210B_LCD_Init()函数后增加一个延时,我建议延时500ms,因为对外设的操作没有我们MCU内部执行速度那么快。
  • LCD_Clear(White):使用白色来清屏
  • LCD_SetTextColor(Blue):设置文字颜色为蓝色,之后显示文字都是蓝色的
  • LCD_SetBackColor(White):设置背景颜色为白色,之后屏显示就是白底蓝字

这几个函数都能在lcd.h中找到,直接复制粘贴过来就行,不用死记硬背:

蓝桥杯嵌入式备赛手册

4.4.2 用LCD进行显示

4.4.2.1 写一个显示函数在循环中不停执行

我推荐的做法是写一个window()函数,在主函数main()的while(1)循环中每隔一段时间执行一次(即刷新屏幕)

比如我要完成这样一个任务:
- 在第0行显示一个float类型的数据(保存两位小数),float类型数据叫做’test_float’
- 在第2行显示”Hello World”
- 每隔200ms刷新一次屏幕

我就这样写window()函数并且调用它:

void window()
{
    u8 str[20];

    LCD_ClearLine(Line0);
    sprintf(str,"test_float: %.2f",test_float);
    LCD_DisplayStringLine(Line0,str);

    LCD_ClearLine(Line2);
    sprintf(str,"Hello World");
    LCD_DisplayStringLine(Line2,str);

}

int main()
{
    ...
    STM3210B_LCD_Init();
    delay_ms(500);
    LCD_Clear(White);
    LCD_SetTextColor(Blue);
    LCD_SetBackColor(White);
    ...

    while(1)
    {
        window();
        delay(200);
    }
}

主要还是要灵活运用

4.4.2.2 高亮显示

再比如,比赛中经常让你高亮显示某一行,高亮就是把蓝色的字设置为绿色或者其他亮一点的颜色的字

那么我们再规定一个任务:
- 在第0行显示一个float类型的数据(保存两位小数),float类型数据叫做’test_float’
- 在第2行行显示一个int类型的数据,int类型数据叫做’test_int’
- 通过按键KEY1选择是float高亮还是int数据高亮,按一下切换一次
- 每隔200ms刷新一次屏幕

我们设置一个全局变量,默认是0:

u8 choose=0;

在KEY1按键中断函数中对全局变量choose进行改变:

void EXTI0_IRQHandler(void)
{
    delay_ms(5);
  if(EXTI_GetITStatus(EXTI_Line0) != RESET)
  {
    if(choose==0)
        {
            choose=1;
        }
        else
        {
            choose=0;
        }
    EXTI_ClearITPendingBit(EXTI_Line0);
  }
}

在window()函数里进行判断,如果是0就对float数据那一行高亮,如果是1就对int数据那一行高亮:

void window()
{
    u8 str[20];

    LCD_ClearLine(Line0);
    sprintf(str,"test_float: %.2f",test_float);
    if(choose==0)
    {
        LCD_SetTextColor(Green);
    }
    LCD_DisplayStringLine(Line0,str);
    LCD_SetTextColor(Blue);

    LCD_ClearLine(Line2);
    sprintf(str,"test_int: %d",test_int);
    if(choose==1)
    {
        LCD_SetTextColor(Green);
    }
    LCD_DisplayStringLine(Line2,str);
    LCD_SetTextColor(Blue);

}

这里请理解按键控制全局变量进行改变,显示函数根据变量来改变屏幕的输出

4.5 UART(串口)驱动

CT117E使用UART2连接外部串行接口,即可以直接使用UART2与上位机通信。

4.5.1 UART硬件连接

蓝桥杯嵌入式备赛手册

总之就是初始化的时候初始化UART2,接受和发送的时候使用UART2即可

4.5.2 UART软件驱动

接收的时候使用中断接收,发送的时候主动发送并查询

usart.c:

void usart_Init()
{
    USART_InitTypeDef USART_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;

    USART_Init(USART2, &USART_InitStructure);

    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);

    USART_Cmd(USART2, ENABLE);
}

void usart_SendString(u8 *str)
{
    u8 i=0;
    do{
        USART_SendData( USART2, str[i]);
        while(!USART_GetFlagStatus( USART2,  USART_FLAG_TXE));
        i++;
    }while(str[i]!=0);
}

void USART2_IRQHandler(void)
{
    u8 temp;
    if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
    {
        USART_ClearITPendingBit(USART2, USART_IT_RXNE);

        temp= USART_ReceiveData(USART2);
    }
}

usart.h:

#ifndef __USART_H
#define __USART_H

#include "stm32f10x.h"

void usart_Init();
void usart_SendString(u8 *str);

#endif

4.5.3 UART驱动的使用

首先在main()函数里初始化UART:

int main()
{
    ...
    usart_Init();
    ...
    while(1)
    {
        ...
    }
}

如果每隔1s通过串口发送一个float类型数据的值(保留两位小数):

int main()
{
    u8 str[20];
    ...
    usart_Init();
    ...
    while(1)
    {
        sprintf(str,"test_float: %.2f",test_float);
        usart_SendString(str);
        delay_ms(1000);
    }
}

如果要接收字符串,给出一个任务:
- 接受一串字符串以字符标志’S’结尾
- 并在LCD上的第8行显示接收的字符串

可以这么做:

u8 RxBuffer[20];
u8 uart_index=0;
u8 receive=0;
void USART2_IRQHandler(void)
{
    u8 temp;
  if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
  {
        USART_ClearITPendingBit(USART2, USART_IT_RXNE);

     temp= USART_ReceiveData(USART2);

    if((temp=='S')||(uart_index==20)) 
    {
            receive=1;
            uart_index=0;

      USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
    }
        else
        {
            RxBuffer[uart_index++]=temp;
        }
  }
}

void window()
{
    u8 str[20];
    if(receive==1)
    {
        LCD_ClearLine(Line8);
        sprintf(str,"%s",RxBuffer);
        LCD_DisplayStringLine(Line8, str);
        receive=0;
        for(i=0;i<20;i++)
            RxBuffer[i]=0;
        USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
    }
}

4.5.4 UART驱动的快速编写

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->USART->Interrupt”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

可以找到对应UART的初始化代码,还可以找到引脚初始化,中断初始化等等的代码:

蓝桥杯嵌入式备赛手册

把接收引脚改成PA3,发送引脚改成PA2即可,中断IRQChannel改成USART2_IRQn,再使能对应的时钟即可。

再打开同目录下的”stm32f10x_it.c”可以找到接收中断函数相关代码:

蓝桥杯嵌入式备赛手册

我们使用查询的方式发送,所以不需要中断发送的相关代码。

4.6 E2PROM的驱动

对E2PROM的考察在近几年频繁出现,主要是E2PROM可以完成掉电存储的功能,这个东西几乎是必考,所以:
- 因为比赛提供I2C驱动,所以了解下I2C协议即可,有兴趣也可以自己写一写I2C通信驱动
- 必须能熟练敲出对E2PROM的读写驱动(不仅限于存储一个字节)

在我这篇博文里有讲如何使用IO口模拟I2C协议:

https://blog.csdn.net/zach_z/article/details/75331275

4.6.1 E2PROM的硬件连接

蓝桥杯嵌入式备赛手册

E2PROM STM32
SCL PB6
SDA PB7

4.6.2 E2PROM的软件驱动

直接把比赛提供的”i2c.c”和”i2c.h”复制粘贴到我们工程目录里的HARDWARE文件夹里,并把i2c.c添加到工程里

在i2c.c中添加E2PROM的读写驱动代码:

void x24_write(u8 add,u8 data)
{
    I2CStart();

    I2CSendByte(0xa0);
    I2CWaitAck();

    I2CSendByte(add);
    I2CWaitAck();

    I2CSendByte(data);
    I2CWaitAck();

    I2CStop();
}

u8 x24_read(u8 add)
{
    u8 data;

    I2CStart();

    I2CSendByte(0xa0);
    I2CWaitAck();

    I2CSendByte(add);
    I2CWaitAck();

    I2CStart();
    I2CSendByte(0xa1);
    I2CWaitAck();

    data=I2CReceiveByte();
    I2CSendAck();

    return data;
}

4.6.2.1 读写后增加延时

这里需要注意,在每次执行完读或者写之后需要加一个延时函数(我延时2ms),因为MCU内部执行速度太快,而E2PROM外设跟不上内部时钟频率。

比如当执行以下代码:

u8 data=0;
data=x24_read(0x01);
data++;

MCU的执行速度太快,而外设反应跟不上,就会导致data可能还没有取得E2PROM里的数据的时候data就开始自增,导致程序预期结果和实际执行结果不一样。

应该在读函数后增加一个2ms的延时函数:

u8 data=0;
data=x24_read(0x01);
delay_ms(2);
data++;

4.6.2.2 读写一个u32类型数据

比赛时不仅仅会让读写一个Byte的数据,可能让读写int型或者float型数据,所以根据我们读写一个Byte数据的驱动来灵活运用是很重要的。

读写一个u32类型的数据

void x24_write_int(u8 add, int data)
{
    x24_write(add,(u8)(data&0xff));
    delay_ms(2);
    x24_write(add+1,(u8)(data>>8&0xff));
    delay_ms(2);
    x24_write(add+2,(u8)(data>>16&0xff));
    delay_ms(2);
    x24_write(add+3,(u8)(data>>24&0xff));
    delay_ms(2);
}

int x24_read_int(u8 add)
{
    int data;
    data=(int)x24_read(add);
    delay_ms(2);
    data+=(int)x24_read(add+1)<<8;
    delay_ms(2);
    data+=(int)x24_read(add+2)<<16;
    delay_ms(2);
    data+=(int)x24_read(add+3)<<24;
    delay_ms(2);
    return data;
}

4.6.3 使用E2PROM驱动

首先要在main函数初始化时对i2c进行一个初始化,其次使用读函数读出我们需要的数据,如果要写就直接写数据即可。

我们规定这样一个任务:
- 每次开机从E2PROM的0x01处读出一个u32类型数据(test_int)
- 通过按键KEY1来存储这个数据的值到E2PROM的0x01处

#include "i2c.h"
u32 test_int=0;
int main()
{
    i2c_init();
    test_int=x24_read_int(0x01);
    while(1)
    {
        ...
    }

}

extern u32 test_int;
void EXTI9_5_IRQHandler(void)
{
    delay_ms(5);
    if(EXTI_GetITStatus(EXTI_Line8) != RESET)
    {
        x24_write_int(0x01,test_int);
        EXTI_ClearITPendingBit(EXTI_Line8);
    }
}

4.7 利用RTC完成计时

RTC在基础板也可能作为一个考察点,比如第九届省赛就让做个电子时钟,,其实也可以用其他计时方法,但是为了比赛准备更加充分,还是要至少能够快速编写RTC驱动。

4.7.1 RTC计时软件驱动

规定这样一个任务:
- 初始时间为 23-50-10
- 到23-59-59后变为00-00-00
- 在LCD屏的Line2上显示时间

这样完成任务:
创建”rtc.c”和”rtc.h”并保存到HARDWARE文件夹下,并在工程中添加”rtc.c”

rtc.c:

#include "rtc.h"
u32 Time=23*3600+50*60+10;
void rtc_Init()
{
    /* Enable PWR and BKP clocks */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

  /* Allow access to BKP Domain */
  PWR_BackupAccessCmd(ENABLE);

  /* Reset Backup Domain */
  BKP_DeInit();

  /* Enable the LSI OSC */
  RCC_LSICmd(ENABLE);
  /* Wait till LSI is ready */
  while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET)
  {}
  /* Select the RTC Clock Source */
  RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);

  /* Enable RTC Clock */
  RCC_RTCCLKCmd(ENABLE);

  /* Wait for RTC registers synchronization */
  RTC_WaitForSynchro();

  /* Wait until last write operation on RTC registers has finished */
  RTC_WaitForLastTask();

  /* Enable the RTC Second */
  RTC_ITConfig(RTC_IT_SEC, ENABLE);

  /* Wait until last write operation on RTC registers has finished */
  RTC_WaitForLastTask();

  /* Set RTC prescaler: set RTC period to 1sec */
  RTC_SetPrescaler(40000);

  /* Wait until last write operation on RTC registers has finished */
  RTC_WaitForLastTask();

  RTC_SetCounter(Time);
}

void RTC_IRQHandler(void)
{
  if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
  {
    if(RTC_GetCounter()==86400)
        {
            RTC_SetCounter(0x0);
        }

    /* Clear Interrupt pending bit */
    RTC_ClearITPendingBit(RTC_FLAG_SEC);
  }
}

led.h:

#ifndef __RTC_H
#define __RTC_H

#include "stm32f10x.h"

void rtc_Init();
extern u32 Time;

#endif

在main中调用RTC初始化函数,并在显示函数中编写相应时间显示代码:

#include "rtc.h"

void window()
{
    u8 str[20];
    Time = RTC_GetCounter();
    sprintf(str1,"%0.2d-%0.2d-%0.2d",Time/3600,(Time%3600)/60,(Time%3600)%60);
    LCD_DisplayStringLine(Line2, str);
}

int main()
{
    ...
    rtc_Init();
    while(1)
    {
        window();
        delay_ms(300);
    }
}

4.7.2 快速编写RTC驱动

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->RTC->LSI_Calib”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

直接复制粘贴RTC_Configuration()部分,去掉最后两句代码,加上我们的计数值设定即可。

在相同目录下,打开”stm32f10x_it.c”还可以看到RTC中断函数相关代码,修改修改直接使用即可:

蓝桥杯嵌入式备赛手册

另外RTC_SetCounter()和RTC_GetCounter()也可以在库函数文件”stm32f10x_rtc.h”里找到,如果忘记可以在那里找:

蓝桥杯嵌入式备赛手册

4.8 定时器中断

定时器中断是非常有用的,不论是在省赛还是在国赛,如果你想让你的嵌入式系统更加完美,运行效果更好,使用定时器中断一定会起到很多作用

4.8.1 TIM4定时器中断代码

tim.c:

#include "tim.h"
void tim4_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

    TIM_TimeBaseStructure.TIM_Period = arr;
    TIM_TimeBaseStructure.TIM_Prescaler = psc;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStructure);

    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);

    TIM_Cmd(TIM4, ENABLE);
}

void TIM4_IRQHandler(void)
{
    u8 str[20];
    if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM4, TIM_IT_Update);

    }
}

tim.h:

#ifndef __TIM_H
#define __TIM_H

#include "stm32f10x.h"

void tim4_Init(u16 arr,u16 psc);

#endif

4.8.2 使用TIM4中断来完成任务

假设我们设计:每隔50ms执行一次我们所有完成的任务(比如使用UART发送字符串”hello\n”,并且进行一次AD采样)

我们使用TIM4来完成每隔50ms的定时器中断,使用TIM4的中断服务函数来执行我们所要执行的任务。

在main函数中初始化TIM4为50ms执行一次,在TIM4的中断服务函数中执行串口发送和AD采样的任务:

#include "tim.h"
int main()
{
    ...
    tim4_Init(499,7199);
    while(1)
    {
        ...
    }

}

void TIM4_IRQHandler(void)
{
    u8 str[20];
    if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)
    {
        usart_SendString(str);
        get_ADC();
        TIM_ClearITPendingBit(TIM4, TIM_IT_Update);

    }
}

4.8.3 快速编写定时器中断驱动

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->TIM->TimeBase”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

  • 取用基础时钟设置的代码
  • 取用中断配置的代码
  • 中断设置改为TIM_IT_Update
  • 增加时钟使能

4.9 ADC

对ADC使用是考察的重点,第九届国赛和第八届国赛都考察到了,分值占比极大。

4.9.1 一路AD采样

如果没有用到扩展板,AD采样应该是会通过基础板上那个电位器R37来调节电压进行捕获考察ADC的使用

4.9.1.1 一路AD采样的硬件连接

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

可以根据数据手册看到,PB0对应着ADC的Channel_8

4.9.1.2 一路AD采样的软件驱动

adc.c:

void adc_Init()
{
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;  
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; 
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_13Cycles5);    

    ADC_Cmd(ADC1, ENABLE);   
    ADC_ResetCalibration(ADC1);

    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);

    while(ADC_GetCalibrationStatus(ADC1));
}

u16 get_adc()
{
    u16 value;

    ADC_SoftwareStartConvCmd(ADC1,ENABLE);
    while(!ADC_GetFlagStatus(ADC1,  ADC_FLAG_EOC));
    value = ADC_GetConversionValue(ADC1);

    return value;
}

adc.h:

#ifndef __ADC_H
#define __ADC_H

#include "stm32f10x.h"

void adc_Init();
u16 get_adc();
#endif

4.9.1.3 一路AD采样的使用

比如每隔100ms对PB0上的电压进行AD采样并计算电压数值(保留2位小数),显示在LCD的Line1行上:

int main()
{
    u16 adc_value;
    u8 str[20];
    ...
    adc_Init();
    while(1)
    {
       adc_value=get_adc();
       sprintf(str," PB0: %.2f V",(float)adc_value/4096*3.3);
       LCD_DisplayStringLine(Line1, str);
       delay_ms(100);
    }
}

4.9.2 两路AD采样

两路AD采样肯定是用到扩展板了,扩展板的

4.9.2.1 两路AD采样的硬件连接

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

扩展板使用跳线帽将PA4和AO1相连,PA5和AO2相连,所以通道4对通过电位器PR5调节的输出电压进行采样;通道5对通过电位器PR6调节的输出电压进行采样

4.9.2.2 两路AD采样的软件驱动

void adc_Init()
{
    ADC_InitTypeDef ADC_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);


     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
     ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 2;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_55Cycles5);

    ADC_DMACmd(ADC1, ENABLE);

    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);

    while(ADC_GetResetCalibrationStatus(ADC1));

     ADC_StartCalibration(ADC1);

    while(ADC_GetCalibrationStatus(ADC1));
}

u16 get_adc(u8 channel)
{
    u16 value;
    ADC_RegularChannelConfig( ADC1,channel,  1,  ADC_SampleTime_55Cycles5);
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    while(!ADC_GetFlagStatus( ADC1,  ADC_FLAG_EOC));
    value=ADC_GetConversionValue( ADC1);
    ADC_SoftwareStartConvCmd(ADC1, DISABLE);
    return value;

}

4.9.2.3 使用两路AD采样驱动

规定任务为:
- 每隔100ms对两路AD进行采样
- 得到两路AD的值,并计算对应电压
- 将AO1的电压显示在LCD的Line3上
- 将AO2的电压显示在LCD的Line4上

int main()
{
    u16 ao1,ao2;
    u8 str[20];
    ...
    adc_Init();
    while(1)
    {
        ao1=get_adc(ADC_Channel_4);
        ao2=get_adc(ADC_Channel_5);
        LCD_ClearLine(Line3);
        sprintf(str," AO1: %.2f V",(float)ao1/4096*3.3);
        LCD_DisplayStringLine(Line3,str);
        LCD_ClearLine(Line4);
        sprintf(str," AO1: %.2f V",(float)ao2/4096*3.3);
        LCD_DisplayStringLine(Line4,str);
        delay_ms(100);
    }
}

4.9.3 快速编写ADC驱动

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->ADC->ADC1_DMA”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

  • 更改我们需要的对应引脚
  • 直接复制粘贴ADC初始化设置部分
  • 把两个ENABLE都改成DISABLE
  • 更改对应的转换通道个数和通道设置

获取adc值函数中用到的ADC库函数都能在”stm32f10x_adc.h”中找到,记不住就可以去复制粘贴

蓝桥杯嵌入式备赛手册

4.10 PWM

PWM是最最最重要的部分,我们训练的目标在于

  • 输出两路占空比可调,频率可调的PWM
  • 捕获两路PWM的频率和占空比

这里一定要玩的非常清楚和熟练,PWM的工作原理我就不介绍了,自行学习

我们要根据不同的题目需求来选择不同方式来捕获pwm或者输出pwm

4.10.1 PWM硬件连接

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

引脚 对应时钟通道 扩展版说明
PA1 TIM2_CH2 可以调节输入PWM的频率
PA2 TIM2_CH3 可以调节输入PWM的频率
PA6 TIM3_CH1 可以调节输入PWM的占空比
PA7 TIM3_CH2 可以调节输入PWM的占空比

4.10.2 PWM软件驱动

4.10.2.1 输出两路频率固定占空比可调的PWM

这种情况直接使用定时器的PWM输出模式即可

假设从PA1和PA2输出,我们的驱动代码为:

void pwm_Init()
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);


    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_AFIO, ENABLE);

    GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_1 | GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOA, &GPIO_InitStructure);

    TIM_TimeBaseStructure.TIM_Period = 999;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC2Init(TIM2, &TIM_OCInitStructure);
    TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);

    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;
    TIM_OC3Init(TIM2, &TIM_OCInitStructure);
    TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM2, ENABLE);

    TIM_Cmd(TIM2, ENABLE);
}
  • 可以看到这里TIM2的预装载值为999,分频系数为71,证明我们的频率为 72Mhz/(72*1000)=1Khz
  • 我们直接设置了TIM2的CH2和CH3为PWM输出模式1,以及输出极性为高
  • 我们通过TIM_SetCompare2()和TIM_SetCompare3()来改变两个通道输出PWM的占空比

比如我们要求:
- PA1输出占空比为10%
- PA2输出占空比为20%
- 按一下KEY1让PA1的占空比增加10%

u16 pa1=100,pa2=200;
int main()
{
    ...
    pwm_Init();
    TIM_SetCompare2(TIM2, pa1);
    TIM_SetCompare3(TIM2, pa2);
    while(1)
    {
        ...
    }
}

void EXTI0_IRQHandler(void)
{
    delay_ms(5);
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        if(pa1<1000)
            pa1+=100;
        TIM_SetCompare2(TIM2, pa1);

        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}
快速写PWM输出模式驱动:

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->TIM->PWM_Output”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

找到能用的复制粘贴过来就行

4.10.2.2 输出两路频率可调占空比可调的PWM

这种情况就可以直接使用比较输出

规定任务:
- 从PA6和PA7输出两路PWM
- PA6频率为10KHz,占空比为10%
- PA7频率为1KHz,占空比为20%
- KEY1使PA6的占空比增加10%
- KEY2使PA7的频率增加1KHz

则我们这样写比较输出的驱动:
pwm.c

__IO uint16_t CCR1_Val =100; //72000000/72/10000
__IO uint16_t CCR2_Val =1000; //72000000/72/1000
float zhankong1=0.1;
float zhankong2=0.2;

void pwm_Init()
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);


    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

    GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6 | GPIO_Pin_7 ;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_TimeBaseStructure.TIM_Period = 65535;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);


    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = CCR1_Val;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
    TIM_OC1Init(TIM3, &TIM_OCInitStructure);

    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Disable);


    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = CCR2_Val;

    TIM_OC2Init(TIM3, &TIM_OCInitStructure);

    TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Disable);

    TIM_Cmd(TIM3, ENABLE);

    TIM_ITConfig(TIM3, TIM_IT_CC1 | TIM_IT_CC2 , ENABLE);
}

uint16_t capture = 0;
u8 pa6_state=0,pa7_state=0;
void TIM3_IRQHandler(void)
{

  if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
  {
    TIM_ClearITPendingBit(TIM3, TIM_IT_CC1 );
    capture = TIM_GetCapture1(TIM3);
        if(pa6_state==0)
        {
            TIM_SetCompare1(TIM3, capture + (u16)CCR1_Val *zhenkong1 );
            pa6_state=1;
        }
        else
        {
            TIM_SetCompare1(TIM3, capture + (u16)CCR1_Val *(1-zhankong1));
            pa6_state=0;
        }
  }


  if (TIM_GetITStatus(TIM3, TIM_IT_CC2) != RESET)
  {
    TIM_ClearITPendingBit(TIM3, TIM_IT_CC2);
    capture = TIM_GetCapture2(TIM3);
        if(pa7_state==0)
        {
            TIM_SetCompare2(TIM3, capture + (u16)CCR2_Val*zhankong2);
            pa7_state=1;
        }
        else
        {
            TIM_SetCompare2(TIM3, capture + (u16)CCR2_Val*(1-zhankong2));
            pa7_state=0;
        }
  }
}

在主程序中调用以及按键的外部中断服务函数为:

int main()
{
    ...
    pwm_Init();
    while(1)
    {
        ...
    }
}
extern float zhankong1;
extern __IO uint16_t CCR2_Val;
u16 pinlv2=1000;
void EXTI0_IRQHandler(void)
{
    delay_ms(5);
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        zhankong1+=0.1
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

void EXTI9_5_IRQHandler(void)
{
    delay_ms(5);
    if(EXTI_GetITStatus(EXTI_Line8) != RESET)
    {
        pinlv2+=1000;
        CCR2_Val=(u16)(1000000/pinlv2);//72MHz/72
        EXTI_ClearITPendingBit(EXTI_Line8);
    }
}
  • 主要就是比较输出的驱动代码,剩下调整频率,调整占空比改变相应的全局变量即可
  • 改变频率就改变CCR1_Val和CCR2_Val的值
  • 改变占空比就改变zhankong1和zhankong2的值
快速写输出比较模式驱动:

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->TIM->OCToggle”文件夹下,打开”main.c”:

蓝桥杯嵌入式备赛手册

  • 基本上比较输出的设置代码都在这里,更改相应的引脚和通道就可以

再打开同目录下的”stm32f10x_it.h”:

蓝桥杯嵌入式备赛手册

  • 这里中断服务函数已经有了输出相应频率的代码
  • 我们对中断服务函数进行扩充增加占空比部分即可

4.10.2.3 捕获一路PWM的频率和占空比

如果只捕获一路PWM直接使用定时器的PWM输入模式即可

比如捕获PA6传来的PWM的频率和占空比:

void capture_Init()
{
    TIM_ICInitTypeDef  TIM_ICInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOA, &GPIO_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x0;

    TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);

    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);

    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

    TIM_SelectMasterSlaveMode(TIM3, TIM_MasterSlaveMode_Enable);

    TIM_Cmd(TIM3, ENABLE);

    TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
}

__IO uint16_t IC2Value = 0;
__IO uint16_t DutyCycle = 0;
__IO uint32_t Frequency = 0;

void TIM3_IRQHandler(void)
{

  TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);


  IC2Value = TIM_GetCapture1(TIM3);

  if (IC2Value != 0)
  {

    DutyCycle = (TIM_GetCapture2 (TIM3) * 100) / IC2Value;


    Frequency = SystemCoreClock / IC2Value;
  }
  else
  {
    DutyCycle = 0;
    Frequency = 0;
  }
}
  • DutyCycle即为占空比
  • Frequency即为频率
  • 可以给时钟分频,但是在计算频率的时候就要除以分频数
快速写PWM输入模式驱动:

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->TIM->OCToggle”文件夹下,”main.c”和”stm32f10x_it.c”分别有着PWM输入模式初始化和中断服务程序相关代码:

蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

  • 只需要更改到你所需的引脚和对应的时钟通道即可

4.10.2.4 捕获两路PWM的频率和占空比

这时通过输入捕获来得到频率和占空比

原理图如下:

蓝桥杯嵌入式备赛手册

比如通过输入捕获得到PA6和PA7的频率和占空比:

void capture_Init()
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
     NVIC_InitTypeDef NVIC_InitStructure;
    TIM_ICInitTypeDef  TIM_ICInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);


  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_7|GPIO_Pin_6;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

  GPIO_Init(GPIOA, &GPIO_InitStructure);;

    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

     TIM_TimeBaseStructure.TIM_Period = 65535;
  TIM_TimeBaseStructure.TIM_Prescaler = 71;
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

  TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);


    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
  TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
  TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
  TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
  TIM_ICInitStructure.TIM_ICFilter = 0x0;

  TIM_ICInit(TIM3, &TIM_ICInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
  TIM_ICInit(TIM3, &TIM_ICInitStructure);



  TIM_Cmd(TIM3, ENABLE);


  TIM_ITConfig(TIM3, TIM_IT_Update|TIM_IT_CC2|TIM_IT_CC1, ENABLE);

}


__IO uint16_t IC3ReadValue12 = 0, IC3ReadValue22 = 0;
__IO uint16_t CaptureNumber2 = 0;
__IO uint32_t Capture2 = 0;
__IO uint32_t TIM3Freq2 = 0;
u8 eage2=0;
u16 rising2=0,falling2=0;
float zhankong2=0;

__IO uint16_t IC3ReadValue11 = 0, IC3ReadValue21 = 0;
__IO uint16_t CaptureNumber1 = 0;
__IO uint32_t Capture1 = 0;
__IO uint32_t TIM3Freq1 = 0;
u8 eage1=0;
u16 rising1=0,falling1=0;
float zhankong1=0;

void TIM3_IRQHandler(void)
{ 
  if(TIM_GetITStatus(TIM3, TIM_IT_CC2) == SET) 
  {
        if(eage2==0)
        {

            if(CaptureNumber2 == 0)
            {

                IC3ReadValue12 = TIM_GetCapture2(TIM3);
                CaptureNumber2 = 1;
            }
            else if(CaptureNumber2 == 1)
            {

                IC3ReadValue22 = TIM_GetCapture2(TIM3); 


                if (IC3ReadValue22 > IC3ReadValue12)
                {
                    Capture2= (IC3ReadValue22 - IC3ReadValue12); 
                }
                else
                {
                    Capture2 = ((0xFFFF - IC3ReadValue12) + IC3ReadValue22); 
                }

                TIM3Freq2 = (uint32_t) SystemCoreClock /72/ Capture2;
                CaptureNumber2 = 0;
            }

            rising2=TIM_GetCapture2(TIM3);
            TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Falling);
            eage2=1;
        }
        else if(eage2==1)
        {
            falling2=TIM_GetCapture2(TIM3);
            if(falling2>rising2)
            {
                zhankong2=(float)(falling2-rising2)/Capture2;
            }
            else if(falling2<rising2)
            {
                zhankong2=(float)(0xFFFF-falling2+rising2)/Capture2;
            }
            eage2=0;
            TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Rising);
        }
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update|TIM_IT_CC2);
  }

    if(TIM_GetITStatus(TIM3, TIM_IT_CC1) == SET) 
  {
        if(eage1==0)
        {

            if(CaptureNumber1 == 0)
            {

                IC3ReadValue11 = TIM_GetCapture1(TIM3);
                CaptureNumber1 = 1;
            }
            else if(CaptureNumber1 == 1)
            {

                IC3ReadValue21 = TIM_GetCapture1(TIM3); 


                if (IC3ReadValue21 > IC3ReadValue11)
                {
                    Capture1= (IC3ReadValue21 - IC3ReadValue11); 
                }
                else
                {
                    Capture1 = ((0xFFFF - IC3ReadValue11) + IC3ReadValue21); 
                }

                TIM3Freq1 = (uint32_t) SystemCoreClock /72/ Capture1;
                CaptureNumber1 = 0;
            }

            rising1=TIM_GetCapture1(TIM3);
            TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling);
            eage1=1;
        }
        else if(eage1==1)
        {
            falling1=TIM_GetCapture1(TIM3);
            if(falling1>rising1)
            {
                zhankong1=(float)(falling1-rising1)/Capture1;
            }
            else if(falling1<rising1)
            {
                zhankong1=(float)(0xFFFF-falling1+rising1)/Capture1;
            }
            eage1=0;
            TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising);
        }
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update|TIM_IT_CC1);
  }
}
  • 频率就是TIM3Freq1和TIM3Freq2
  • 占空比就是zhankong1和zhankong2
快速写输入捕获模式驱动:

在比赛提供的V3.5库的”Project->STM32F10x_StdPeriph_Examples->TIM->InputCapture”文件夹下,”main.c”和”stm32f10x_it.c”分别有着输入捕获模式初始化和中断服务程序相关代码:
蓝桥杯嵌入式备赛手册

蓝桥杯嵌入式备赛手册

  • 对比我在上面放的驱动和输入捕获官方例程,能用的就复制粘贴
这里再次强调,要把PWM的输入输出完全掌握并且驱动越熟越好
第八届国赛中考察了两路频率可变的捕获和频率可变的输出
两路占空比的捕获和占空比可变的PWM捕获和输出可变的PWM将很有可能在之后的比赛中碰到

4.11 扩展板的按键矩阵

扩展板的8个按键根据AD采样到相应引脚的电压来判断是哪一按键

前面ADC驱动已经讲过怎么写了,就跳过ADC的部分

扩展版按键通过跳线帽将PA5与AKEY相连

这8个按键需要注意的地方:
- 如果按照板子上的电阻确实是跟原理图上阻值一模一样,那么通过计算就可以得到每个按键按下的电压值
- 如果板子上电阻阻值跟原理图上有偏差,那么直接开始试,从小到大开始一个一个按键试
- 8个按键试出来也花不了10分钟,这是我得出的解决办法

直接贴出扩展版按键驱动:

#include "button.h"

void button_Init()
{
    ADC_InitTypeDef ADC_InitStructure;

    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);


    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_55Cycles5);

    ADC_Cmd(ADC1, ENABLE);  
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

u8 get_button()
{
    u16 btn_tmp;
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    while(!ADC_GetFlagStatus( ADC1,  ADC_FLAG_EOC));
    btn_tmp=ADC_GetConversionValue(ADC1);

    if(btn_tmp <= 0x0020)
    {
        return 1;
    }
    else if((btn_tmp >= 0x00B0) && (btn_tmp <= 0x0100))
    {
        return 2;
    }
    else if((btn_tmp >= 0x0240) && (btn_tmp <= 0x0300))
    {
        return 3;
    }
    else if((btn_tmp >= 0x03B0) && (btn_tmp <= 0x0450))
    {
        return 4;
    }
    else if((btn_tmp >= 0x0450) && (btn_tmp <= 0x0700))
    {
        return 5;
    }
    else if((btn_tmp >= 0x0700) && (btn_tmp <= 0x0800))
    {
        return 6;
    }
    else if((btn_tmp >= 0x0840) && (btn_tmp <= 0x0940))
    {
        return 7;
    }
    else if(btn_tmp <= 0x0B50)
    {
        return 8;
    }
    else
    {
        return 0;   //error status & no key
    }
}

在主程序中循环扫描的办法来判断是否有按键按下

int main()
{
    u8 button;
    ...
    button_Init();
    while(1)
    {
        button=get_button();
        ...
    }
}

4.12 数码管

数码管在官方给的声明通知中说只考察静态,不管考不考,我们准备好,知道怎么写驱动总归是不亏的。

4.12.1 数码管的硬件连接

蓝桥杯嵌入式备赛手册

PA1~3用跳线帽跟P3上的对应引脚相连即可

4.12.2 数码管的软件驱动

seg.c

#include "seg.h"

u8 seg[17]={ 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00}; 
void seg_Init()
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_ResetBits(GPIOA,GPIO_Pin_1);
    GPIO_ResetBits(GPIOA,GPIO_Pin_2);
    GPIO_ResetBits(GPIOA,GPIO_Pin_3);
}

void seg_Control(u8 bit1, u8 bit2, u8 bit3)
{
    u8 temp,i;

    temp=seg[bit3];
    for(i=0;i<8;i++)
    {
        if(temp&0x80)SER_H;
        else SER_L;

        temp=temp<<1;
        SCK_L;
        delay_ms(1);
        SCK_H;
    }

    temp=seg[bit2];
    for(i=0;i<8;i++)
    {
        if(temp&0x80)SER_H;
        else SER_L;

        temp=temp<<1;
        SCK_L;
        delay_ms(1);
        SCK_H;
    }

    temp=seg[bit1];
    for(i=0;i<8;i++)
    {
        if(temp&0x80)SER_H;
        else SER_L;

        temp=temp<<1;
        SCK_L;
        delay_ms(1);
        SCK_H;
    }

    RCK_L;
    delay_ms(1);
    RCK_H;
}

seg.h:

#ifndef __SEG_H
#define __SEG_H

#include "stm32f10x.h"


#define SER_H GPIO_SetBits(GPIOA,GPIO_Pin_1)
#define SER_L GPIO_ResetBits(GPIOA,GPIO_Pin_1)

#define RCK_H GPIO_SetBits(GPIOA,GPIO_Pin_2)
#define RCK_L GPIO_ResetBits(GPIOA,GPIO_Pin_2)

#define SCK_H GPIO_SetBits(GPIOA,GPIO_Pin_3)
#define SCK_L GPIO_ResetBits(GPIOA,GPIO_Pin_3)

void seg_Init();
void seg_Control(u8 bit1, u8 bit2, u8 bit3);

#endif

4.13 ds18b20驱动的使用

ds18b20是一个温度传感器

因为比赛提供ds18b20的驱动文件,所以直接把.c和.h文件添加到我们的HARDWARE文件夹下,并且添加进工程即可。

蓝桥杯嵌入式备赛手册

PA6通过跳线帽与P3上对应引脚相连就可以驱动ds18b20温度传感器了

使用ds18b20获取温度后显示在LCD的第一行:

#include "ds18b20.h"
int main()
{
    u32 z;
    u8 str[20];
    ...
    ds18b20_init_x();
    while(1)
    {
        z=(ds18b20_read()&0x7ff);
        LCD_ClearLine(Line0);
        sprintf(str," t= %2.2f",(float)z/16.);
        LCD_DisplayStringLine(Line0, str);
    }
}

4.14 DHT11驱动的使用

DHT11是一个温湿度传感器

因为比赛提供dht11的驱动文件,所以直接把.c和.h文件添加到我们的HARDWARE文件夹下,并且添加进工程即可。

蓝桥杯嵌入式备赛手册

PA7通过跳线帽与P3上对应引脚相连就可以驱动dht11温湿度传感器了

规定一个任务
- 使用dht11驱动获得适度显示在LCD第一行
- 获得温度显示在LCD第二行

#include "dht11.h"
int main()
{
    u32 z;
    u8 str[20];
    ...
    dht11_init();
    while(1)
    {
        z=dht11_read();
        LCD_ClearLine(Line1);
        sprintf(str1," shidu= %2d %%",z>>24);
        LCD_DisplayStringLine(Line1, str1);
        LCD_ClearLine(Line2);
        sprintf(str1," wendu= %3d ",(z>>8)&0xff);
        LCD_DisplayStringLine(Line2, str1);
        delay_ms(2000);
    }
}
  • 记得这个温湿度读取一次要等待一段时间才能顺利读取到下一次的数据(一般2s)

4.15 三轴传感器

这个东西一般不会考察,就像蜂鸣器一样,考察蜂鸣器考场会乱成一锅粥,这个东西也要拿着板子转来转去,而且它占用引脚也多,但是保不齐这几年把扩展板玩的没什么可考察的了,就出个三轴传感器的题目,最好还是要掌握一下驱动怎么写。

蓝桥杯嵌入式备赛手册

  • PA4~7都不能作为其他用处,三周传感器需要使用到这四个引脚资源
  • P2全部短接

这个外设的通信协议也是I2C跟我们之前说到的E2PROM一样,所以我们就轻车熟路了。

主要是以下两点:
- 更改I2C驱动里的SDA和SCL引脚
- 正确配置三轴传感器

修改I2C的引脚定义:
i2c.c:

/** I2C 总线接口 */
#define I2C_PORT GPIOA      
#define SDA_Pin GPIO_Pin_5  
#define SCL_Pin GPIO_Pin_4 

驱动代码为:

u8 alz[3] ;

//
void LIS302DL_Write(unsigned char reg,unsigned char info)
{   
    I2CStart(); 
    I2CSendByte(0x38);  
    I2CWaitAck();     
    I2CSendByte(reg);  
    I2CWaitAck(); 
    I2CSendByte(info); 
    I2CWaitAck();
    I2CStop();
}

//
uint8_t LIS302DL_Read(uint8_t address)
{
    unsigned char val;
    I2CStart(); 
    I2CSendByte(0x38);  
    I2CWaitAck();     

    I2CSendByte(address);  
    I2CWaitAck(); 

    I2CStart();
    I2CSendByte(0x39); 
    I2CWaitAck();
    val = I2CReceiveByte(); 
    I2CSendNotAck();
    I2CStop();

    return(val);
}

//
u8* Lis302DL_Output(void)
{
    if((LIS302DL_Read(0x27) & 0x08) != 0)
    {

        alz[0] = (LIS302DL_Read(0x29));  //x
        alz[1] = (LIS302DL_Read(0x2B));  //y
        alz[2] = (LIS302DL_Read(0x2D));  //z
    }

    return alz;
}

//
void LIS302DL_Config(void)
{
    LIS302DL_Write(CTRL_REG1,0x47);
    LIS302DL_Write(CTRL_REG2,0x00);
    LIS302DL_Write(CTRL_REG3,0xC1);
    LIS302DL_Write(FF_WU_THS_1,0x28);       
    LIS302DL_Write(FF_WU_DURATION_1,40);        
// LIS302DL_Write(FF_WU_CFG_1,0x10); 
}

//
uint8_t LIS302DL_Check(void)
{
    if(LIS302DL_Read(0x0f))
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

在main()函数中使用,并在LCD上打印出三个轴的角度:

int main()
{
    u8 *p;
    u8 str1[20];
    ...
    i2c_init();
    LIS302DL_Config();
    if(LIS302DL_Check() == 1)
    {
        LCD_DisplayStringLine(Line1, (u8 *)" MEMS STATUS: OK");
    }
    else
    {
        LCD_DisplayStringLine(Line1, (u8 *)" MEMS STATUS: ERROR");

    }
    while(1)
    {
      p=Lis302DL_Output();

      LCD_ClearLine(Line2);
      sprintf(str1," X= %d ",(int)p[0]);
      LCD_DisplayStringLine(Line2, str1);
      LCD_ClearLine(Line3);
      sprintf(str1," Y= %d ",(int)p[1]);
      LCD_DisplayStringLine(Line3, str1);
      LCD_ClearLine(Line4);
      sprintf(str1," Z= %d ",(int)p[2]);
      LCD_DisplayStringLine(Line4, str1);  
    }
}

4.16 光敏电阻

这个也没什么讲的,要考察也不大可能考察到这里,看一眼驱动就行了

一共有两个光敏电阻,一个直接读引脚高低判断光强,一个是根据输入电压AD采样断光强

蓝桥杯嵌入式备赛手册

PA3直接读取引脚电平判断光强弱:

void DO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

//
int main(void)
{   
    SysTick_Config(SystemCoreClock/1000);

    STM3210B_LCD_Init();
    LCD_Clear(White);

    LCD_SetTextColor(White);
    LCD_SetBackColor(Blue);

    LCD_ClearLine(Line0);
    LCD_ClearLine(Line1);
    LCD_ClearLine(Line2);
    LCD_ClearLine(Line3);
    LCD_ClearLine(Line4);

    LCD_DisplayStringLine(Line1,(u8*)" R-Photo DEMO ");

    LCD_SetTextColor(Blue);
    LCD_SetBackColor(White);

    DO_Config();

    while(1)
    {

        if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) == Bit_RESET)
        {
            LCD_DisplayStringLine(Line7, (u8*)" DO:High ");
        }
        else
        {
            LCD_DisplayStringLine(Line7, (u8*)" DO:Low ");
        }
    }
}

PA4根据AD采样的值来判断光强大小,ADC之前有讲这里就不说了:

int main(void)
{   

    u8 str[20];
    u16 tmp = 0;

    SysTick_Config(SystemCoreClock/1000);

    STM3210B_LCD_Init();
    LCD_Clear(White);

    LCD_SetTextColor(White);
    LCD_SetBackColor(Blue);

    LCD_ClearLine(Line0);
    LCD_ClearLine(Line1);
    LCD_ClearLine(Line2);
    LCD_ClearLine(Line3);
    LCD_ClearLine(Line4);

    LCD_DisplayStringLine(Line1,(u8*)" R-Photo DEMO ");

    LCD_SetTextColor(Blue);
    LCD_SetBackColor(White);

    ADC_Config();

    while(1)
    {
        tmp = Read_ADC();
        snprintf((char *)str, sizeof(str), " R-P:%.2fK ", tmp/(4096.-tmp)*10);
        LCD_DisplayStringLine(Line6, str);
        Delay_Ms(200);
    }
}

五、客观题

5.1 片上资源

片内资源是可以通过数据手册查找到的,具体可以打开考场提供数据手册”stm32f103rbt6.pdf”查看:

蓝桥杯嵌入式备赛手册

  • 128K闪存(flash)
  • 20K SRAM
  • 3个通用定时器(TIM2、TIM3、TIM4)
  • 一个高级定时器(TIM1)
  • 2个SPI (SP1、SPI2)
  • 2个I2C (I2C1、I2C2)
  • 3个USART (USART1、USART2、USART3)
  • 1个USB 2.0
  • 1个CAN 2.0B
  • 49个GPIO端口
  • 2个(12位ADC模块),都是16个通道
  • CPU频率是72MHz
  • 工作电压:2.0~3.6V
  • 工作温度: -40 ~ +85 / -40 ~ +105
  • 封装形式:LQFP64

5.2 可能会考到的stm32知识

可能考到的基本都在数据手册能找到,比赛时应该是提供英文版手册,我建议在比赛前把手册都看一遍,至少能知道对应知识点在哪里找。

5.2.1 嵌套的向量式中断控制器(NVIC)

蓝桥杯嵌入式备赛手册

STM32F103xx增强型产品内置嵌套的向量式中断控制器,能够处理多达43个可屏蔽中断通道(不包括 16个Cortex™-M3的中断线)和16个优先级。

  • 紧耦合的NVIC能够达到低延迟的中断响应处理
  • 中断向量入口地址直接进入内核
  • 紧耦合的NVIC接口
  • 允许中断的早期处理
  • 处理晚到的较高优先级中断
  • 支持中断尾部链接功能
  • 自动保存处理器状态
  • 中断返回时自动恢复,无需额外指令开销

记住16个优先级,5个优先级分组

5.2.2 外部中断/事件控制器(EXTI)

外部中断/事件控制器包含19个边沿检测器,用于产生中断/事件请求。每个中断线都可以独立地配置 它的触发事件(上升沿或下降沿或双边沿),并能够单独地被屏蔽;有一个挂起寄存器维持所有中断请 求的状态。EXTI可以检测到脉冲宽度小于内部APB2的时钟周期。多达80个通用I/O口连接到16个外 部中断线。

5.2.3 时钟和启动

统时钟的选择是在启动时进行,复位时内部8MHz的RC振荡器被选为默认的CPU时钟,随后可以 选择外部的、具失效监控的4~16MHz时钟;

当检测到外部时钟失效时,它将被隔离,系统将自动地 切换到内部的RC振荡器,如果使能了中断,软件可以接收到相应的中断。

同样,在需要时可以采取 对PLL时钟完全的中断管理(如当一个间接使用的外部振荡器失效时)。

多个预分频器用于配置AHB的频率、高速APB(APB2)和低速APB(APB1)区域。

AHB和高速APB的最 高频率是72MHz,低速APB的最高频率为36MHz。

参考如图的时钟驱动框图:

蓝桥杯嵌入式备赛手册

最重要的是要通过时钟框图找到时钟源和相应设备的关系:

比如40KHz的RC振荡器HSI作为独立看门狗(IWDG)的时钟源:

蓝桥杯嵌入式备赛手册

5.2.4 DMA

DMA基本在客观题内算是必考,因为DMA是嵌入式里一个重要的传输手段,而主观题又不好考察,所以一定会在客观题上考察DMA的相应知识点

灵活的7路通用DMA可以管理存储器到存储器、设备到存储器和存储器到设备的数据传输;DMA控 制器支持环形缓冲区的管理,避免了控制器传输到达缓冲区结尾时所产生的中断。

每个通道都有专门的硬件DMA请求逻辑,同时可以由软件触发每个通道;传输的长度、传输的源地 址和目标地址都可以通过软件单独设置。

DMA可以用于主要的外设:SPI、I 2 C、USART,通用、基本和高级控制定时器TIMx和ADC。

STM32 的 DMA 有以下一些特性:
1. 每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能 通过软件来配置。
2. 在七个请求间的优先权可以通过软件编程设置 (共有四级:很高、高、中等和低),假如 在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。
3. 独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和 目标地址必须按数据传输宽度对齐。
4. 支持循环的缓冲器管理
5. 每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。
6. 存储器和存储器间的传输
7. 外设和存储器,存储器和外设的传输
8. 闪存、SRAM、外设的 SRAM、APB1 APB2 和 AHB 外设均可作为访问的源和目标。
9. 可编程的数据传输数目:最大为 65536

5.2.5 看门狗

5.2.5.1 独立看门狗

独立的看门狗是基于一个12位的递减计数器和一个8位的预分频器,它由一个内部独立的40kHz的RC 振荡器提供时钟

因为这个RC振荡器独立于主时钟,所以它可运行于停机和待机模式。它可以被当 成看门狗用于在发生问题时复位整个系统,或作为一个*定时器为应用程序提供超时管理。

通过 选项字节可以配置成是软件或硬件启动看门狗。在调试模式下,计数器可以被冻结。

5.2.5.2 窗口看门狗

窗口看门狗内有一个7位的递减计数器,并可以设置成*运行。它可以被当成看门狗用于在发生问 题时复位整个系统。

它由主时钟驱动,具有早期预警中断功能;在调试模式下,计数器可以被冻结

5.3 其它方面的知识

客观题没办法速成,还是在于平常的积累,我们只能有针对性的去了解一些方面
- 同步通信和异步通信的区别
- 嵌入式系统的存储体系
- 串行总线和并行总线的区别
- 实时操作系统的知识
- 数组电路的知识
- 模拟电路的知识和计算(主要是二极管、三极管、运算器、放大器)

这些知识掌握的越多越好

六、比赛心得

6.1 牢记各个引脚的功能

可以外接的引脚一共只有8个(PA1-7),并且跟扩展板相连是驱动扩展板所有外设的引脚

相信在练习比赛模拟题的过程中,脑子里就会逐渐对PA1-7越来越熟。

虽说这些引脚对应的功能,对应的时钟通道我们都能在相应手册上查到,但是比赛时寸秒寸金,希望能在脑子里有下面一个表格:

GPIO 功能
PA1 TIM2_CH2 / SER(74LS595串行数据输入引脚) / 频率可调脉冲
PA2: TIM2_CH3 / USART2_TX / RCK(74LS595串行存储时钟输入引脚) / 频率可调脉冲
PA3 USART2_RX / SCK(74LS595串行移位时钟输入引脚) / 光敏电阻读入引脚
PA4 ADC1_IN4 / SCL(三轴传感器时钟引脚)
PA5 ADC1_IN5 / SDA(三轴传感器数据引脚) / 4*2按键矩阵
PA6 TIM3_CH1 / 占空比可调脉冲 / 温度传感器
PA7 TIM3_CH2 / 占空比可调脉冲 / 温湿度传感器

6.2 如何备赛

总体路线:
- 首先先把每个独立的驱动搞懂,能够做到顺利编写出来并运用
- 其次再做前几年的模拟题、省赛题、国赛题上
- 最后给自己出题,出自己认为可能考的部分,但是前几年没考的

6.2.1 快速编写驱动

如果你看了我上面对每个驱动代码的编写,你就能知道其实大部分的代码我们都不需要自己敲。

比赛提供的库函数的Example中就有着我们基本需要驱动的代码,我们只需要更改到我们需要的(时钟、引脚等等)即可。

一些函数如果忘记怎么写,对应库函数的头文件也可以找到。

6.2.2 每做一次新的赛题重新开始

希望每次重新做一道主观题时,都能从重新构建工程->重新编写每个驱动代码->写逻辑层上的代码

因为比赛时和平常做东西不一样,即便你认为自己原理都懂、驱动都知道怎么写,如果没有做到非常熟练,难免会出现小失误,而小失误有可能会耽误大量的时间

所以既然选择了参加比赛,那么我们就期望准备约充分越好,对工程建立约熟练越好、基础驱动编写越快越好

6.2.3 对备赛时间的分配要有规划

如果你是小白,那么一定是越早开始备赛越好,一方面学习stm32的应用知识,另一方面不断地练习完成一个嵌入式系统

如果你有一定的51、stm32或是其他嵌入式设备的开发基础,那么根据你的学习能力来分配备赛时间

我省赛前一共用了两周时间大概是这么分配的:
- 第一周完成工程建立和基础驱动的编写(每天建一次工程,完成一到两个小项)
- 我的小项准备顺序(LED -> Systick(delay_ms()) -> KEY -> LCD -> ADC -> RTC -> USART -> E2PROM(I2C) )
- 第二周开始练习往届题目,做了14~17年四年间的一些模拟题和省赛题

刚开始做模拟题肯定会很慢,7、8个小时才能完成任务,训练两三届的题就会越来越顺手了

6.3 如何比赛

6.3.1 不要提前离场

比赛时候总有一些同学提前离场,也不知道是全部做出信心满满还是做不出来直接放弃

我认为既然已经选择了来比赛,比赛前也做了那么多的准备,就应该尽最大的努力去完成比赛

如果已经做出来了程序的所有任务,觉着比赛稳了:
- 可以再检查检查自己的客观题,毕竟国二以上都是在客观题上分高低
- 还可以检查检查自己的程序,多测试几组数据看看有无bug
- 还可以修改修改代码,简化算法,添加注释什么的

总之,准备几个星期就为了这5个小时,提前走总有些得不偿失

6.3.2 对比赛时间的分配要有规划

省赛每个地方不一样,可能不会让提前进场,我就假设5个小时的时间:
- 拿到题后先看一遍主观题,在心里留下个印象
- 开始做客观题,客观题一般也不会耗费多少时间,20分钟顶天了,在做客观题的时候大脑也在想主观题的框架
- 做完客观题记得提交一遍,开始做主观题写程序
- 如果提前做完当然好,检查检查,优化优化,排查一切可能出现的bug
- 如果临比赛结束还有半小时没写完,先把现有的程序做成能顺利运行不会宕机的程序,提交一遍
- 一定要保证自己在还有半小时结束时客观题和程序都提交了一遍,先保证自己有个分

国赛一般提前半小时可以进赛场,排队进,早排队的大概能比最后的早进十几分钟,所以尽量早去多争取十几分钟:
- 提前进场后,不给发板子,但是我们可以利用这20分钟先建立个工程,写个LED和按键的驱动(LED和按键肯定要考)
- 充分利用进场的时间,如果做的好,应该可以完成工程建立、LED、按键、LCD全部的工作
- 拿到板子后,先上电测一下看看是否正常
- 拿到赛题后,先看一遍主观题,直接做客观题,做完一定记得提交
- 国赛的后半小时会乱糟糟,很多人提前离场,所以还是在离结束半小时时调通程序,一定要提交一遍所有客观题和程序

再次说明一下离结束半小时提交的重要性:
- 后半小时是提交高峰,赛点服务器拥堵,如果放到最后几分钟提交可能会有提交不成功现象发生
- 后半小时很多人提前交卷离场,来来回回进进出出,万一哪个麻瓜碰了一下你的电源线,直接GG
- 后半小时理论上不会有什么太大的改变,但是你提交过一次后,最起码心里有底,不会很慌,说不定还会有突破,解决了没解决的问题

6.3.3 碰到问题千万不要放弃

所有比赛基本上都会碰到问题:ACM、电赛、我们参加的蓝桥杯都一样,都会碰到或大或小的问题

要明确一点:
- 如果一点难度都没有就拿奖证明你的水平已经高于这个比赛了,那么你拿奖是理所应当,并没有值得高兴的
- 碰到问题,遇到困难,想办法解决了,获得最后成功才是最快乐的

碰到问题一定要用自己掌握的知识和训练的经验去一步步解决,冷静分析,不要放弃

6.4 第八届嵌入式国赛代码例子

根据实例来理解我写的各个驱动的使用方法,代码可以到网盘直接下载:
链接:https://pan.baidu.com/s/1EF33Tzc3KdsccduUnqJNSQ 密码:6rv3

最后祝愿各位参赛的同学都能有个理想的成绩

                                            ——西安邮电大学 张凯捷