# 1 day1

# 1.1 基本概念

MCU 微控制器

MPU 微处理器(手机芯片)

FLASH 闪存,永久保存数据,保存程序

RAM 随机存取存储器,临时保存数据

JTAG 烧录(下载)、调试程序

ROM 只读存储器

最小系统组成:主控芯片、晶振电路、供电电路、复位电路、调试下载电路、boot 启动电路

# 1.2 ARM

Advanced RISC Machine 高级精简指令集机器,是一种体系结构

RISC 精简指令集,功耗更低,多数指令可以在单时间周期内执行完成

CISC 复杂指令集

ARM 英国芯片公司,设计更低功耗、更高性能的芯片。开创 chipless 的先河。

设计 ARM 体系结构,设计内核。不生产不销售芯片。通过授权体系结构和内核收费营利。

ARM 指令集 ---- 32 位

Thumb 指令集 ---- 16 位

Thumb-2 指令集 ---- 16/32 位混合

# 1.3 体系结构与内核、芯片的关系

基于体系结构设计内核

基于内核设计芯片

每个芯片都关联三个编号:芯片编号、内核编号、体系结构编号

如 STM32F103RCT6 是芯片编号,它的内核编号是 Cortex-M3,体系结构编号是 ARMv7M

# 1.4 三级流水线

执行指令的过程可以分解为:取指、译码、执行,三个阶段。

这三个阶段分别由 CPU 内部的不同部件执行。CPU 的执行部件在执行第一条指令时,译码部件可以进行第二条指令的译码,而取指部件则可以对第三条指令进行取指,这样三个部件同时都在工作,不造成浪费。

# 1.5 独立看门狗和窗口看门狗

看门狗提供一种机制:当系统发生异常程序跑飞,可以自动复位,重新运行。

keil 生成 bin:C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o .\Objects\demo.bin .\Objects\demo.axf

# 2 day2

# 2.1 交叉开发

软件:keil、mcuisp

硬件:usb、st-link、j-link

# 2.2 准备工作

  1. 环境搭建:keil、板级支持包
  2. 烧录工具:keil/mcuisp、st-link 等
  3. 需求:完成的功能、功耗等。
  4. 资料:原理图、mcu 芯片参考手册、数据手册、内核编程手册、用户手册(固件函数库)、其他外设、传感器手册。

# 2.3 GPIO

通用 I/O 功能

GPIO 是 MCU 中的一种设备,通过它可以直接控制 MCU 的 IO 口(引脚)

STM32F103RCT6 有 GPIOA, GPIOB, GPIOC, GPIOD 共 4 个 GPIO 接口。

每个 GPIO 接口最多可以管理 16 个 IO 口。

对 GPIO 接口的控制,可以通过 7 个寄存器来实现:

. 两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH) . 两个16位数据寄存器(GPIOx_IDR,GPIOx_ODR) . 一个32位置位/复位寄存器(GPIOx_BSRR), . 一个16位复位寄存器(GPIOx_BRR) . 一个32位锁定寄存器(GPIOx_LCKR)

复用功能

复用功能:一脚多用(通用功能,外设功能)。比如:

14 号引脚,通用 IO 是 PA0,外设功能有:

WKUP

USART2_CTS

ADC123_IN0

TIM2_CH1_ETR

TIM5_CH1

TIM8_ETR

输入模式

  1. 输入浮空
  2. 输入上拉
  3. 输入下拉
  4. 模拟输入

输入时,输入状态由外部电路决定,上拉时,如果外部浮空,得到高电平,下拉时,如果外部浮空得到低电平。

输出断开。

输出模式

  1. 推挽输出。
  2. 开漏输出。
  3. 复用推挽输出。(用于外设)
  4. 复用开漏输出。(用于外设)

开漏输出可以实现:线条与功能、电流型驱动,输出 0,得到低电平,输出 1,IO 口电平由外部电路决定。

一般输出数字信号时配置为推挽,需要“线与”功能时配置为开漏。

一个 IO 口需要 4 个位来配置模式,16 个 IO 口需要 64 个位,因此共需要两个 32 位寄存器,

CRL 寄存器配置 IO0 ~ IO7,CRH 寄存器配置 IO8 ~ IO15。

对输出数据寄存器 ODR 写的值,可以通过 ODR 读取。

为什么要通过 BSRR 寄存器和 BRR 寄存器来操作 ODR?

确保对 ODR 的写入是原子的,中途不会被中断,因此软件不需要因为对 ODR 的写入而禁止中断。

当对GPIOx_ODR的个别位编程时,软件不需要禁止中断:在单次APB2写操作里,可以只更改一个或多个位。这是通过对“置位/复位寄存器”(GPIOx_BSRR,复位是 GPIOx_BRR)中想要更改的位写’1’来实现的。没被选择的位将不被更改。

标准库中将对寄存器的操作封装在函数中,因此调用标准库函数也就操作了寄存器。

# 2.4 volatile

编译器可能对变量有以下优化:

  1. 对同一变量的连续多次赋值时,可能只保留最后一次赋值,而优化掉前面几步的赋值。
  2. 重新排列对变量操作的语句
  3. 对变量的访问,为了加快速度,将变量保存到高速缓存中,之后从高速缓存中访问这个变量

使用 volatile 修饰的变量,编译器就不会进行以上优化。

# 2.5 调试

使用 keil 逻辑分析仪:

Dialog DLL: DARMSTM.DLL

Parameter: -pSTM32F103RC

点击 setup,如果查看 GPIOC.9 变化情况,则输入 PORTC.9。

DisplayType 选择 bit

点击 in,out,调节 Grid。

# 2.6 重映射

查看数据手册,得知,56 号引脚 PB4 默认功能为 NJTRST

因为使用 st-link 使用 sw 模式烧录程序,不用 NJTRST,JTDO,所以可以重定义 pb4,pb3 ,pa15 为普通 IO 口。

需要打开 AFIO 时钟。

GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);

# 3 day3

# 3.1 位带

位带别名区就是为位带区服务的,对位带别名区的操作最终都会反映在位带区上,我们操作位带别名区的时候就等价于在操作位带区地址。

有两个位带区域:

位带区域 位带别名区域
0x20000000-0x200FFFFF(1M) 0x22000000-0x23FFFFFF(32M) SRAM地址
0x40000000-0x400FFFFF(1M) 0x42000000-0x43FFFFFF(32M) 外设寄存器地址

stm32 是数据总线宽度是 32 位,字长 4 字节,按字寻址速度最快,所以用一个字映射一位。

在位带别名区里,只要最低位是1,那么对应的位带区的位就是1。

翻译自编程手册:

下列公式展示了别名区如何映射到位带区的:

bit_word_offset = (byte_offset x 32) + (bit_number x 4)
bit_word_addr = bit_band_base + bit_word_offset
1
2
  • bit_word_offset 是目标位在位带内存区的位置。
  • bit_word_addr 是在位带内存区中映射到目标位的字的地址。
  • bit_band_base 是别名区的起始地址。
  • byte_offset 是包含目标位的字节在位段里的序号。
  • bit_number 是目标位的位置,0~7 中之一。

使用示例:

#define BIT_WORD_OFFSET(byte_offset, bit_number)\
    (((byte_offset) << 5) + ((bit_number) << 2))

#define BIT_WORD_ADDR(bit_band_base, byte_offset, bitnumber)\
    ((bit_band_base) + BIT_WORD_OFFSET(byte_offset, bitnumber))

#define ADDR(addr) (*((volatile unsigned int *) (addr)))

#define PAin(bit_number) ADDR(BIT_WORD_ADDR(PERIPH_BB_BASE, (GPIOA_BASE & 0x000FFFFF) + 0x08, bit_number))
#define PAout(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOA_BASE & 0x000FFFFF) + 0x0C, bit_number))

#define PBin(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOB_BASE & 0x000FFFFF) + 0x08, bit_number))
#define PBout(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOB_BASE & 0x000FFFFF) + 0x0C, bit_number))

#define PCin(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOC_BASE & 0x000FFFFF) + 0x08, bit_number))
#define PCout(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOC_BASE & 0x000FFFFF) + 0x0C, bit_number))

#define PDin(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOD_BASE & 0x000FFFFF) + 0x08, bit_number))
#define PDout(bit_number) ADDR(BIT_WORD_ADDR((GPIOA_BASE & 0xF0000000) + 0x02000000, \
(GPIOD_BASE & 0x000FFFFF) + 0x0C, bit_number))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 3.2 启动模式

三种启动模式:用户闪存存储器,系统存储器,SRAM

无论哪一种启动模式,最终都是映射到 0x00000000 地址执行。

用户闪存存储器保存我们录到 MCU 的程序。

系统存储器保存芯片生产线上烧录的启动程序。

SRAM 是将程序复制到其内运行。

选择启动模式:

boot1 boot0 启动位置 存储地址
x 0 用户闪存存储 0x08000000
0 1 系统存储器 0x1FFFFFFF
1 1 SRAM 0x20000000

# 3.3 启动流程

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

                PRESERVE8
                THUMB


; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                各种异常、中断处理函数...

; SystemInit() 位于 system_stm32f10x.c 文件中,主要进行系统时钟的配置、闪存、内存的配置。
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

0x00000000 地址读取栈顶指针(加载到 SP 寄存器)

0x00000004 地址读取复位处理函数 Reset_Handler(),并跳转执行(加载到 PC 寄存器)

Reset_Handler() 函数中先调用 SystemInit() 函数,结束后返回;再调用 main() 函数,永不返回。

SystemInit() 函数中主要做三件初始化工作:设置系统时钟,配置 FLASH,配置外部 SRAM。

SystemInit() 函数中执行 SetSysClk() 函数:

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
  SetSysClock();
1
2
3

# 3.4 复位

三种复位方式:系统复位、电源复位、备份域复位。

系统复位将复位除时钟控制寄存器CSR中的复位标志和备份区域中的寄存器以外的所有寄存器

当以下事件中的一件发生时,产生一个系统复位:

  1. NRST管脚上的低电平(外部复位)
  2. 窗口看门狗计数终止(WWDG复位)
  3. 独立看门狗计数终止(IWDG复位)
  4. 软件复位(SW复位)
  5. 低功耗管理复位

可通过查看RCC_CSR控制状态寄存器中的复位状态标志位识别复位事件来源。

其中软件复位:

通过将Cortex™-M3中断应用和复位控制寄存器(SCB_AIRCR)中的SYSRESETREQ位置’1’,可实现软件复位。

其中低功耗管理复位:

在以下两种情况下可产生低功耗管理复位:

  1. 在进入待机模式时产生低功耗管理复位:通过将用户选择字节中的nRST_STDBY位置’1’将使能该复位。这时,即使执行了进入待机模式的过程,系统将被复位而不是进入待机模式。
  2. 在进入停止模式时产生低功耗管理复位:通过将用户选择字节中的nRST_STOP位置’1’将使能该复位。这时,即使执行了进入停机模式的过程,系统将被复位而不是进入停机模式。

电源复位:

当以下事件中之一发生时,产生电源复位:

  1. 上电/掉电复位(POR/PDR复位)
  2. 从待机模式中返回

电源复位将复位除了备份区域外的所有寄存器。

备份域复位:

当以下事件中之一发生时,产生备份区域复位。备份域复位只影响备份区域。

  1. 软件复位,备份区域复位可由设置备份区域控制寄存器RCC_BDCR中的BDRST位产生。
  2. 在VDD和VBAT两者掉电的前提下,VDD或VBAT上电将引发备份区域复位。

core_cm3.h 中定义了以下软件复位函数:

#define SCB_AIRCR_VECTKEY_Pos     16
#define SCB_AIRCR_PRIGROUP_Pos    8
#define SCB_AIRCR_PRIGROUP_Msk    (7ul << SCB_AIRCR_PRIGROUP_Pos)
#define SCB_AIRCR_SYSRESETREQ_Pos 2
#define SCB_AIRCR_SYSRESETREQ_Msk (1ul << SCB_AIRCR_SYSRESETREQ_Pos)
static __INLINE void NVIC_SystemReset(void)
{
  SCB->AIRCR  = ((0x5FA << SCB_AIRCR_VECTKEY_Pos)      | /* 对 VECTKEY 域写 0x5FA 关键字 */
                 (SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) | /* 保持优先级分阻不变 */
                 SCB_AIRCR_SYSRESETREQ_Msk);             /* 将 SYSRESETREQ 位置 1 */
  __DSB();  /* 设置内存屏障,确保之前的变量访问完成 */
  while(1); /* 等待复位 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13

对 SCB->AIRCR 寄存器写操作时,必须对高 16 位(VECTKEY域)写 0x5FA 关键字。

# 3.5 时钟

三种不同的时钟源可以被用来驱动系统时钟(sysclk):

  1. HSI 内部高速 RC 振荡器时钟 如 8MHz。
  2. HSE 外部高速振荡器时钟,如 8MHz。
  3. PLL 锁相环时钟,引入时钟源进行倍频产生系统所需的高速时钟。

以下设备拥有二级时钟源:

  1. 40kHz低速内部RC,可以用于驱动独立看门狗和通过程序选择驱动RTC。RTC用于从停机/待机模式下自动唤醒系统。(LSI 内部低速 RC 时钟源)
  2. 32.768kHz低速外部晶体也可用来通过程序选择驱动RTC(RTCCLK)。(LSE 外部低速晶振时钟源)

当不被使用时,任一个时钟源都可被独立地启动或关闭,由此优化系统功耗。

SystemInit() 函数内调用 SetSysClock() 函数设置系统时钟。

又因为定义了 SYSCLK_FREQ_72MHz 宏,所以,最终调用的是 SetSysClockTo72();

SetSysClockTo72() 做了以下工作:

  1. 使能 HSE
  2. 等待外部高速时钟 HSE 稳定
  3. 配置 FLASH
  4. 配置总线时钟:HCLK = SYSCLK / 1,PCLK2 = HCLK / 1,PCLK1 = HCLK / 2。其中 HCLK 是 AHB 总线时钟,PCLK2 是 APB2 总线时钟,PCLK1 是 APB1 总线时钟
  5. 配置 PLL 为 HSE 的 9 倍频:PLLCLK = HSE * 9 = 72 MHz
  6. 使能 PLL
  7. 等待 PLL 就绪
  8. 选择 PLL 为系统时钟源 SYSCLK
  9. 等待 PLL 成为系统时钟源
static void SetSysClockTo72(void)
{
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;

  /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/
  /* Enable HSE */ // 使能 hse
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);

  /* Wait till HSE is ready and if Time out is reached exit */
  // 等待 HSE 稳定,如果超时就退出
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;
    StartUpCounter++;
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

  if ((RCC->CR & RCC_CR_HSERDY) != RESET)
  {
    HSEStatus = (uint32_t)0x01;
  }
  else
  {
    HSEStatus = (uint32_t)0x00;
  }

  if (HSEStatus == (uint32_t)0x01)
  {
    /* Enable Prefetch Buffer */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* Flash 2 wait state */
    // 配置 flash
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;


    /* HCLK = SYSCLK */
    // 配置 AHB 时钟
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;

    /* PCLK2 = HCLK */
    // 配置 APB2 时钟
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

    /* PCLK1 = HCLK */
    // 配置 APB1 时钟
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

#ifdef STM32F10X_CL
    /* Configure PLLs ------------------------------------------------------*/
    /* PLL2 configuration: PLL2CLK = (HSE / 5) * 8 = 40 MHz */
    /* PREDIV1 configuration: PREDIV1CLK = PLL2 / 5 = 8 MHz */

    RCC->CFGR2 &= (uint32_t)~(RCC_CFGR2_PREDIV2 | RCC_CFGR2_PLL2MUL |
                              RCC_CFGR2_PREDIV1 | RCC_CFGR2_PREDIV1SRC);
    RCC->CFGR2 |= (uint32_t)(RCC_CFGR2_PREDIV2_DIV5 | RCC_CFGR2_PLL2MUL8 |
                             RCC_CFGR2_PREDIV1SRC_PLL2 | RCC_CFGR2_PREDIV1_DIV5);

    /* Enable PLL2 */
    RCC->CR |= RCC_CR_PLL2ON;
    /* Wait till PLL2 is ready */
    while((RCC->CR & RCC_CR_PLL2RDY) == 0)
    {
    }


    /* PLL configuration: PLLCLK = PREDIV1 * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)~(RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL);
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLSRC_PREDIV1 |
                            RCC_CFGR_PLLMULL9);
#else
    /*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    // 配置 PLL 锁相环为HSE 的倍频,且为 72 Mhz
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
#endif /* STM32F10X_CL */

    /* Enable PLL */
    // 使能 pll 锁相环
    RCC->CR |= RCC_CR_PLLON;

    /* Wait till PLL is ready */
    // 等待 pll 就绪
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }

    /* Select PLL as system clock source */
    // 选择 PLL 作为时钟源
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;

    /* Wait till PLL is used as system clock source */
    // 等待 PLL 被用作时钟源
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
    {
    }
  }
  else
  { /* If HSE fails to start-up, the application will have wrong clock
         configuration. User can add here some code to deal with this error */
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

各总线频率:

SysClk = 72MHz

HCLK = 72MHz / 1

PCLK2 = 72MHz / 1

PCLK1 = 72MHz / 2 = 36MHz

# 3.6 系统滴答定时器 SysTick

参考内核编程手册。

处理器拥有 24 位的系统定时器,SysTick,能从重载值倒数到 0,它在下一个时钟边缘时重载 LOAD 寄存器 里的值,然后在随后的时钟倒计时。

SysTick定时器,是一个简单的定时器,对于CM3、CM4内核芯片,都有SysTick定时器。常用来做延时,或者实时系统的心跳时钟。 这样可以节省MCU资源,不用浪费一个定时器。比如uCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用SysTick做uCOS心跳时钟。

具有自动重载和溢出中断功能。

一共4个Systick寄存器

CTRL            SysTick 控制和状态寄存器

LOAD           SysTick 自动重装载除值寄存器

VAL              SysTick 当前值寄存器

CALIB            SysTick 校准值寄存器

SysTick 控制和状态寄存器 CTRL

对于STM32,外部时钟源是 HCLK(AHB总线时钟)的1/8,内核时钟是 HCLK时钟。

重装载数值寄存器- LOAD

当前值寄存器- VAL

使用系统滴答定时器实现的 delay():

#ifndef _MY_DELAY_H
#define _MY_DELAY_H
#include <stddef.h>

enum CLK_SOURCE{
    AHB_DIV8 = 0,
    AHB_DIV1
};

extern void kdelay_init(enum CLK_SOURCE clk_source);
extern void kdelay_s(size_t s);
extern void kdelay_ms(size_t ms);
extern void kdelay_us(size_t us);

#endif  // _MY_DELAY_H

#include "stm32f10x.h"
#include "my_delay.h"

static size_t us_fac = 0;   //< 微秒因子,每微妙计数几次

void kdelay_init(enum CLK_SOURCE clk_source)
{
    // 9MHz
    SysTick->CTRL = 0x0;
    if (clk_source == AHB_DIV1) {
        // 72MHz
        SysTick->CTRL |= 0x3 << 1;
        us_fac = 72;
    } else {
        us_fac = 9;
    }
    SysTick->LOAD = 0x0;
    SysTick->VAL = 0x0;
}
void kdelay_s(size_t s)
{
    while (s--) {
        kdelay_ms(1000);
    }
}

void kdelay_ms(size_t ms)
{
    while (ms--) {
        kdelay_us(1000);
    }
}
// us <= 233016 ,1 分频
// us <= 1864135 ,8 分频
void kdelay_us(size_t us)
{
    SysTick->VAL = 0x0;
    SysTick->LOAD = us * us_fac;
    // 启动定时器。
    SysTick->CTRL |= 0x1;
    // 读取 countflag 的值,等待计数到 0
    while (!(SysTick->CTRL & 0x1 << 16));
    // 关闭定时器并清除到期标志
    SysTick->CTRL &= ~((0x1 << 16) | (1 << 0));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 3.7 按键去抖动

void key_init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   //< 上拉输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;   //< PA0
    GPIO_Init(GPIOA, &GPIO_InitStructure);

}

bool key_pressed(void)
{
    if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) {
        // 按键消抖动
        kdelay_ms(5);
        while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET);
        kdelay_ms(5);
        return true;
    }
    return false;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 4 day4

# 4.1 USART

处理器与外部设备通信的两种方式:

  1. 串行通信,速度慢,占用引脚资源少。
  2. 并行通信,速度快,占用引脚资源多。

STM32F103RCT6 最多可提供 5 路串口(USART1,USART2,USART3,UART4,UART5)。

串行通信的通信方式

  • 同步通信:带时钟同步信号传输。
    • SPI,IIC通信接口
  • 异步通信:不带时钟同步信号。
    • UART(通用异步收发器),单总线

UART 特点:

  • 全双工异步通信。
  • 小数波特率发生器系统,提供精确的波特率。
  • 可编程的数据字长度(8位或者9位);
  • 可配置的停止位(支持1或者2位停止位);
  • 可配置的使用DMA多缓冲器通信。
  • 单独的发送器和接收器使能位。
  • 检测标志:① 接收缓冲器 ②发送缓冲器空 ③传输结束标志
  • 多个带标志的中断源。触发中断。
  • 其他:校验控制,四个错误检测标志。
  • 多处理器通信 -- 如果地址不匹配,则进入静默模式
  • 从静默模式中唤醒(通过空闲总线检测或地址标志检测)
  • 两种唤醒接收器的方式:地址位(MSB,第9位),总线空闲

USART 是串行、异步、全双工通信协议。

协议包含:起始位,数据字(8 位或 9 位),奇偶校验位,停止位(1,1.5 或 2 位)

从起始位到停止位所有位称为一帧。

双方通信的速度通过波特率控制,即每秒发送的位数。

数据传输速度 = 波特率 / 每帧的位数

nCTS 和 nRTS 两个引脚可实现流量控制,接收方 nRTS 与发送方 nCTS 连接,

接收方通过 nRTS 高电平通知发送方暂停发送,低电平通知发送,

发送方通过读取 nCTS 高电平得知暂停发送,低电平可以发送。

帧格式

STM32串口异步通信定义的参数传送格式:

  1. 起始位
  2. 数据位(8位或者9位)
  3. 奇偶校验位(第8或第9位)
  4. 停止位(1,1.5,2位)
  5. 波特率设置
  • 起始位:发送器是通过发送起始位而开始一个字符的传送。起始位使数据线处于“space”状态
  • 数据位:起始位之后就传送数据位。在数据位中,低位在前(左),高位在后(右)。由于字符编码方式的不同,数据位可以是5、6、7或8位。
  • 奇偶校验位:用于对字符传送作正确性检查,因此奇偶校验位是可选择的,共有3种可能,即奇校验、偶校验和无校验,由用户根据需要选定。
  • 停止位:停止位在最后,用以标志一个字符传送的结束,它对应于“mark”状态。停止位可能是1,1.5或2位,在实际应用中根据需要确定。

1 baud = 1 bit/s

一帧结束后不再传输下一帧,而是维持高电平空闲状态,这称为空闲状态,空闲状态可用于在数据传输完毕时进行数据处理,而避免每收到一个字符就要处理一次,浪费 CPU 资源。

流控

nCTS 和 nRTS 两个引脚可实现流量控制,接收方 nRTS 与发送方 nCTS 连接,接收方通过 nRTS 高电平通知发送方暂停发送,低电平通知发送,发送方通过读取 nCTS 高电平得知暂停发送,低电平可以发送。

串口状态

在对数据进行发送和接收的时候,要检查USART的状态,只有等到数据发送或接收完毕之后才能进行下一帧数据的发送或接收。

串口的状态可以通过状态寄存器 USART_SR 读取。

当数据从 TDR 装入发送寄存器中后,即设置发送空标志 TXE,TXE 标志一般用于单缓冲区(非DMA),当数据从移位寄存器中移出并且 停止位也发送完成标志 TC,TC 标志一般多用于多缓冲区(DMA)。

当数据从接受移位寄存器中装入 RDR 时,硬件设置为非空标志 RXNE。

寄存器

状态寄存器 USART_SR:

TXE:发送数据寄存器空 (Transmit data register empty)

0:数据还没有被转移到移位寄存器;1:数据已经被转移到移位寄存器。注意:单缓冲器传输n中使用该位。

TC:发送完成 (Transmission complete)

0:发送还未完成;1:发送完成。

数据寄存器里面有数据时,TXE就会由硬件自动置0,而TC不会,TC 是判断移位寄存器的。

TXE是TDR寄存器数据转移到移位寄存器后置位;TC是在TXE置位后,并且数据帧传输完成;

RXNE:读数据寄存器非空 (Read data register not empty)

0:数据没有收到;1:收到数据,可以读出。

IDLE:监测到总线空闲 (IDLE line detected)

0:没有检测到空闲总线;1:检测到空闲总线。注意:IDLE位不会再次被置高直到RXNE位被置起(即又检测到一次空闲总线)

数据寄存器 USART_DR

包含了发送或接收的数据。由于它是由两个寄存器组成的,一个给发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。

发送时,把TDR内容转移到发送移位寄存器,由发送移位寄存器一位一位发出;接收时,把收到的每一位保存到接收移位寄存器然后再转移到RDR。

实例

#include <stddef.h>
#include <stdint.h>
#include "stm32f10x.h"

void usart1_init(size_t baud)
{
    // 使能 GPIOA 和 USART1 时钟。
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;

    // 配置 PA9 为复用推挽输出。
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    // 配置 PA10 为输入浮空。
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA,&GPIO_InitStructure);

    // 配置 USART1
    USART_InitTypeDef USART_InitStructure;
    // 波特率
    USART_InitStructure.USART_BaudRate = baud;
    // 收发模式
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    // 停止位一位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    // 奇校验
    USART_InitStructure.USART_Parity = USART_Parity_Odd;
    // 数据位 8 位
    USART_InitStructure.USART_WordLength = USART_WordLength_9b;
    // 无流控
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    // 初始化串口
    USART_Init(USART1, &USART_InitStructure);
    // 使能串口
    USART_Cmd(USART1, ENABLE);
}

// 发送一个字节。
void usart1_putchar(uint8_t ch)
{
    // 等待发送数据寄存器为空。
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, ch);
}
// 发送一个字符串。
void usart1_putstr(const uint8_t *str)
{
    // 等待发送数据寄存器为空。
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    while (*str != '\0') {
        USART_SendData(USART1, *str);
        str++;
        // 等待发送完成。
        while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    }
}

uint8_t usart1_getchar()
{
    // 等待有数据可读。
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
    // 读取数据。
    return USART_ReceiveData(USART1);
}


// main.c

usart1_init(115200);
    usart1_putstr("你好,世界!\n");

    uint8_t ch;
    while (true) {
        ch = usart1_getchar();
        if (ch =='0') {
            pb4_high();
        } else pb4_low();
        usart1_putchar(ch);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

# 4.2 中断

CPU执行程序时,由于发生了某种随机的事件(外部或内部),引起CPU暂时中断正在运行的程序,转去执行一段特殊的服务程序(中断服务子程序或中断处理程序),以处理该事件,该事件处理完后又返回被中断的程序继续执行,这一过程称为中断。

中断进入,由硬件完成。每条机器指令执行完毕时,机器会检查有没有中断产生,如果没有就继续执行下一条指令,如果有中断产生,就保存现场(一些寄存器的值),然后按这个中断的向量表位置找到它的中断处理函数,并跳转执行。

中断处理,由软件实现。程序员实现中断处理程序。

中断退出,由软件完成。中断处理函数结束时,编译器会自动添加代码,恢复现场(恢复进入中断时保存的寄存器值),从中断返回到原来被中断的位置,继续执行。

硬件保存现场,软件恢复现场。

# 4.3 NVIC

NVIC和SCB都是内核组件,其说明在Cortex-M3内核编程手册中。

  • NVIC :嵌套向量中断控制器,用于总体管理异常
  • Cortex-M3内核支持256个中断,其中包含了16个内核中断和240个外部中断,并且具有256级的可编程中断设置。
  • STM32F1并没有使用Cortex-M3内核的全部东西,而是只用了它的一部分。
  • STM32F10x总共有76个中断
  • STM32F10x的76个中断里面,包括16个内核中断和60个可屏蔽中断。
  • 16 个可编程优先级(使用了 4 位中断优先级)

Cortex-M3 共有 16 + 240 = 256 个中断,并且有 256 个中断优先级(使用 8 个位表示)

STM32F1 系列只有 16 + 60 = 76 个中断,只有 16 级中优先级(使用 8 个位中的 [7:4] 4 个位)

优先级数值越小级别越高,数值越大级别越低。

中断管理

对每个中断设置一个抢占优先级和一个响应优先级值。分组配置是在寄存器SCB->AIRCR中配置。

一般情况下,系统代码执行过程中,只设置一次中断优先级分组(只执行一次设置),比如分组2,设置好分组之后一般不会再改变分组。随意改变分组会导致中断管理混乱,程序出现意想不到的执行结果。

抢占优先级 & 响应优先级区别:

  • 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。
  • 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断。
  • 抢占优先级相同的中断,当两个中断同时发生的情况下,哪个响应优先级高,哪个先执行。
  • 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行;

例:

假定设置中断优先级组为2,然后设置中断3(RTC中断)的抢占优先级为2,响应优先级为1。 中断6(外部中断0)的抢占优先级为3,响应优先级为0。中断7(外部中断1)的抢占优先级为2,响应优先级为0。

中断7>中断3>中断6。

中断分组

  • 第0组:所有4位用于指定响应优先级
  • 第1组:最高1位用于指定抢占式优先级,最低3位用于指定响应优先级
  • 第2组:最高2位用于指定抢占式优先级,最低2位用于指定响应优先级
  • 第3组:最高3位用于指定抢占式优先级,最低1位用于指定响应优先级
  • 第4组:所有4位用于指定抢占式优先级
// nvic 初始化
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
// nvic 中断分组
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
// usart 中断配置
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
// usart 中断标志获取
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
// usart 中断标志清除
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);

1
2
3
4
5
6
7
8
9
10
11

注意中断服务函数名必须与.s启动文件中中断向量表(位于启动汇编文件)中的相同,可将函数写在 stm32f10x_it.c 文件中。

中断函数注意:

  • 没有参数
  • 没有返回值
  • 名称必须与中断向量表中完全相同

中断处理函数要快速处理退出,因此不要进行以下工作:

  • 不要进行大量的浮点运算
  • 不要休眠
  • 不要进行输入输出操作(可能造成阻塞或休眠)

示例:

// 全局区默认初始化为 0
volatile uint8_t usart1_recv[RECV_SIZE];   //< 缓冲区
volatile size_t usart1_recv_index;         //< usart1 向缓冲区放数据的坐标

void usart1_it_init(size_t baud)
{
    // 使能 GPIOA 和 USART1 时钟。
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;

    // 配置 PA9 为复用推挽输出。
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    // 配置 PA10 为输入浮空。
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA,&GPIO_InitStructure);

    // 配置 USART1
    USART_InitTypeDef USART_InitStructure;
    // 波特率
    USART_InitStructure.USART_BaudRate = baud;
    // 收发模式
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    // 停止位一位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    // 奇校验
    USART_InitStructure.USART_Parity = USART_Parity_Odd;
    // 数据位 8 位
    USART_InitStructure.USART_WordLength = USART_WordLength_9b;
    // 无流控
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    // 初始化串口
    USART_Init(USART1, &USART_InitStructure);
    //USART 串口中断配置
    // 使能数据就绪可读中断。
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
    // 使能检测到空闲线路中断。
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    // 使能串口
    USART_Cmd(USART1, ENABLE);

    // NVIC 配置
    NVIC_InitTypeDef NVIC_InitStructure;
    // 中断通道为 串口 1 中断
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    // 设置抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
    // 设置响应优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
    // 使能 NVIC
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

    // 初始化 NVIC
    NVIC_Init(&NVIC_InitStructure);
}

// 中断函数

// usart1 中断处理函数
#include <string.h>
#include "system/usart_it.h"
#include "system/remap.h"
void USART1_IRQHandler(void)
{
    volatile int32_t temp;
    // 判断中断标志
    // 如果是数据就绪可读标志
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
        // 缓冲区满了下标就置 0。
        if (usart1_recv_index >= RECV_SIZE) {
            usart1_recv_index = 0;
        }
        usart1_recv[usart1_recv_index] = USART_ReceiveData(USART1);
        usart1_recv_index++;
        // 对中断标志位清零。
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);

    } else
        // 如果检测到空闲线路。
        if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET) {
            // 读取写入的字符串
            if (strncmp(usart1_recv, "on",usart1_recv_index) == 0) {
                pa15_high();
            } else if (strncmp(usart1_recv, "off",usart1_recv_index) == 0) {
                pa15_low();
            }
            usart1_recv_index = 0;
        // 对空闲中断标志位清零。
        //USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 这条语句无效。
        temp = USART1->SR;
        temp = USART1->DR;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 4.4 EXTI

外部中断/事件控制器

stm32f103rct6 有19个能产生事件/中断请求的边沿检测器。每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。

  • EXTI线0~15:对应外部IO口的输入中断。
  • EXTI线16连接到PVD输出
  • EXTI线17连接到RTC闹钟事件
  • EXTI线18连接到USB唤醒事件

一条中断线的在同一时间只能被一个IO口映射:

GPIOx.0映射到EXTI0,GPIOx.1映射到EXTI1,……,GPIOx.15映射到EXTI15

外部中断配置一般步骤

  1. 使能AFIO时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
  2. 初始化IO口为输入。GPIO_Init();
  3. 设置IO口与中断线的映射关系。void GPIO_EXTILineConfig();
  4. 初始化线上中断,设置触发条件等。EXTI_Init();
  5. 配置中断分组(NVIC),并使能中断。NVIC_Init();
  6. 编写中断服务函数。EXTIx_IRQHandler();
  7. 清除中断标志位EXTI_ClearITPendingBit();

例子:

红外计数器:

void counter_sensor_init(void)
{
    // 使能 GPIOB 时钟和 AFIO 时钟。
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
    // PB3 设置为浮空输入。
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // AFIO 外部中断引脚选择 3 号
    // 现在 PB3 引脚的电平信号可以顺利通过 AFIO 进入到 EXTI 电路了。(EXTI3)
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource3);

    EXTI_InitTypeDef EXTI_InitStructure;
    // 外部中断引脚是 Pin3
    EXTI_InitStructure.EXTI_Line = EXTI_Line3;
    // 下降沿触发
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    // 使能中断
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    // 中断类型:中断
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_Init(&EXTI_InitStructure);

    // NVIC 配置
    NVIC_InitTypeDef NVIC_InitStructure;
    // 指定通道为 3
    NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
    // 使能通道
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    // 设置抢占优先级 1
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    // 设置响应优先级 1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
}
// 红外计数器中断处理
#include "drivers/rgb_led.h"
void EXTI3_IRQHandler(void)
{
    red_toggle();
    // 清除中断标志位。
    EXTI_ClearITPendingBit(EXTI_Line3);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 5 day5

# 5.1 定时器

stm32f103rc 有 8 个定时器,4个通用定时器(TIM2,TIM3,TIM4,TIM5),2 个高级控制定时器(TIM1,TIM8),2 个基本定时器(TIM6,TIM7)

# 5.2 基本定时器

基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。它们可以作为通用定时器提供时间基准,特别地可以为数模转换器(DAC)提供时钟。实际上,它们在芯片内部直接连接到DAC并通过触发输出直接驱动DAC。这2个定时器是互相独立的,不共享任何资源。

TIM6 和 TIM7 是一个 16 位的只能向上计数的定时器,只能定时,没有外部 IO。

只有大容量的 stm32f101xx 和 stmf103 产品才有基本定时器。

基本定时器使用内部时钟源,向上计数,可用于定时,主要用于驱动 DAC。

只需配置预分频和周期。

观察时钟树,TIM234567 的频率是 72MHz。

基本定时器是 APB1 总线上的外设,APB1 总线是 2 分频 AHB 总线的, 因此它的频率 = fPCLK1 * 2 = 36MHz * 2 = 72MHz

时钟源(TIMxCLK)

定时器时钟 TIMxCLK,即内部时钟 CK_INT,经 APB1 预分频器后分频提供,如果APB1 预分频系数等于 1,则频率不变,否则频率乘以 2,库函数中 APB1 预分频的系数是 2,即 PCLK1=36M,所以定时器时钟 TIMxCLK=36*2=72M。

计数器时钟(CK_CNT)

定时器时钟经过 PSC 预分频器之后,即 CK_CNT,用来驱动计数器计数。PSC 是一个16 位的预分频器,可以对定时器时钟 TIMxCLK 进行 1~65536 之间的任何一个数进行分频。具体计算方式为:CK_CNT=TIMxCLK/(PSC+1)。

计数器(CNT)

计数器 CNT 是一个 16 位的计数器,只能往上计数,最大计数值为 65535。当计数达到自动重装载寄存器的时候产生更新事件,并清零从头开始计数。计数器从0开始计数至arr(重装载)值,产生溢出事件。

自动重装载寄存器(ARR)

自动重装载寄存器 ARR 是一个 16 位的寄存器,这里面装着计数器能计数的最大数值。当计数到这个值的时候,如果使能了中断的话,定时器就产生溢出中断。

基本定时器编程

查询参考手册可得知,基本定时器的时基单元包含:

  • 计数器寄存器(TIMx_CNT)
  • 预分频寄存器(TIMx_PSC)
  • 自动重装载寄存器(TIMx_ARR)

时钟源经过预分频器分频后才是定时器时钟(可实现 1 ~ 65536 分频)。

自动重装载定时器,就是定时器周期。在事件生成时更新到影子寄存器。可设置范围为 0 至 65535。

typedef struct
{
  uint16_t TIM_Prescaler; 		 //预分频系数 PSC
  uint16_t TIM_CounterMode;		//计数模式
  uint16_t TIM_Period;			//定时器周期 ARR,自动重装载
  uint16_t TIM_ClockDivision; 	//外部输入时钟分频
  uint8_t TIM_RepetitionCounter; //重复计数,高级计时器才需要配置
} TIM_TimeBaseInitTypeDef;

1
2
3
4
5
6
7
8
9

基本定时器只能向上计数,使用内部时钟源,只需要配置预分频系数和定时器周期。

例,需要定时器每 1ms 中断一次,生成 1ms 的时钟。

总线时钟最大为72MHz,体现在16位的定时器上的效果就是从0计数到65535上溢只需要0.9毫秒。

65536次 / 72Mhz = 0.91022...ms,定时器最大只能计 0.9 ms。

如果我们需要更长时间的定时间隔,那么就需要预分频器对时钟进行分频处理,以降低定时器时钟(CK_CNT)的频率。

通过配置预分频器,来获取想要的定时器时钟频率。

如将时钟源进行 72 分频,则 72MHz / 72 = 1MHz,每秒计数 1M次,每周期 1us,定时器最大能计时 65535 个周期, 65535 / 1M = 65.535 ms

预分频器的工作的工作原理是,定时器时钟源每tick一次,预分频器计数器值+1,直到达到预分频器的设定值,然后再tick一次后计数器归零,同时,CNT计数器值+1。

由此可以看出,因为达到最大值后还要再tick一次才归零,所以定时器时钟频率应该为Fosc/(PSC+ 1)。其中Fosc是定时器的时钟源。比如想对时钟源进行72分频,那么预分频器的值就应该设置为71。

定时 1ms ,则设置:

TIM_TimeBaseInitStructure.TIM_Period    = 1000;     // 周期 ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 71;       // 预分频 71,每周期 1us
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStructure);
1
2
3

定时器中断配置:

  *     @arg TIM_IT_Update: TIM update Interrupt source
  *     @arg TIM_IT_CC1: TIM Capture Compare 1 Interrupt source
  *     @arg TIM_IT_CC2: TIM Capture Compare 2 Interrupt source
  *     @arg TIM_IT_CC3: TIM Capture Compare 3 Interrupt source
  *     @arg TIM_IT_CC4: TIM Capture Compare 4 Interrupt source
  *     @arg TIM_IT_COM: TIM Commutation Interrupt source
  *     @arg TIM_IT_Trigger: TIM Trigger Interrupt source
  *     @arg TIM_IT_Break: TIM Break Interrupt source
  *   - TIM6 and TIM7 can only generate an update interrupt.
  *   - TIM9, TIM12 and TIM15 can have only TIM_IT_Update, TIM_IT_CC1,
  *      TIM_IT_CC2 or TIM_IT_Trigger.
  *   - TIM10, TIM11, TIM13, TIM14, TIM16 and TIM17 can have TIM_IT_Update or TIM_IT_CC1.
  *   - TIM_IT_Break is used only with TIM1, TIM8 and TIM15.
  *   - TIM_IT_COM is used only with TIM1, TIM8, TIM15, TIM16 and TIM17.

//TIM_IT_Update:更新中断,计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
//TIM_IT_CC1~4:都是捕获/比较中断,貌似都是平等的,即输入捕获,输出比较
//TIM_IT_Trigger:触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
//使用的时候都是调用函数TIM_ITConfig()来使能指定的中断类型,调用TIM_GetITStatus()函数来查看是否有中断发生,入口参数都是平等的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

基本定时器只能生成更新中断。

例子:

定时器按键去抖:

key.h:

#ifndef _KEY_H
#define _KEY_H
#include <stdbool.h>
#include <stdint.h>
// 按键按下则 key_flag >> 0 = 1
// 按键松开则 key_flag >> 1 = 1
extern volatile uint8_t key1_flag;
extern void key_init(void);
extern bool key_pressed(void);
#endif  // _KEY_H
1
2
3
4
5
6
7
8
9
10

key.c:

#include <stdbool.h>
#include "stm32f10x.h"
#include "system/my_delay.h"

volatile uint8_t key1_flag;

void key_init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   //< 上拉输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;   //< PA0
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 中断配置
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

    // NVIC 初始化
    NVIC_InitTypeDef NVIC_InitStructure;

    // EXTI 0
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStructure);

    // EXTI Config
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    EXTI_InitTypeDef EXTI_InitStructure;
    // 外部中断引脚是 Pin0
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    // 双边沿触发
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
    // 使能中断
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    // 中断类型:中断
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_Init(&EXTI_InitStructure);

}

// 按下,松开 返回 true
bool key_pressed(void)
{
    // 定时器实现消抖
    if ((key1_flag & (0x1 << 0))) {
        // 等待到按键松开
        while (!(key1_flag & (0x1 << 1)));
        // 清 0
        key1_flag = 0;
        return true;
    }
    return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

tim.htim.c

#ifndef _TIM_H
#define _TIM_H
#include <stdint.h>
// 基本定时器
extern void tim7_init(uint16_t period, uint16_t prescaler);
extern void tim7_start(uint16_t period);
extern void tim7_end(void);
#endif  // _TIM_H

// tim.c
#include <stdint.h>
#include "stm32f10x.h"

extern void tim7_end(void);

void tim7_init(uint16_t period, uint16_t prescaler)
{
    // NVIC 初始化
    NVIC_InitTypeDef NVIC_InitStructure;

    NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStructure);

    // 使能 TIM7 时钟 APB1
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);

    // 时基单元初始化(计数器寄存器(固定向上)、预分频寄存器、自动重装载寄存器)
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    // 定时器周期
    TIM_TimeBaseInitStructure.TIM_Period =      period;
    // 定时器预分频器设置
    TIM_TimeBaseInitStructure.TIM_Prescaler =   prescaler;
    TIM_TimeBaseInit(TIM7, &TIM_TimeBaseInitStructure);

    // TIM7 中断配置
    void tim7_end();
}

void tim7_start(uint16_t period)
{
    // 设置周期
    TIM7->ARR = period;
    // 开启中断
    TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);
    // 使能 TIM7 定时器
    TIM_Cmd(TIM7, ENABLE);
}
void tim7_end(void)
{
    // 失能中断
    TIM_ITConfig(TIM7, TIM_IT_Update, DISABLE);
    // TIM 7 失能
    TIM_Cmd(TIM7,DISABLE);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

中断处理函数:

#include "drivers/key.h"
#include "system/tim.h"
void EXTI0_IRQHandler(void)
{
    if (SET == EXTI_GetITStatus(EXTI_Line0)) {
        // 屏蔽 EXTI0 中断,按键中断
        EXTI->IMR &= ~1;

        // 对于 EXTI 中断必须手动清除中断标志
        // void EXTI_ClearITPendingBit(uint32_t EXTI_Line)
        EXTI_ClearITPendingBit(EXTI_Line0);

        // 下降沿,按键按下
        if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) {
            // 标志位
            key1_flag |= 0x1u;
            // 定时器消抖动 5ms
            tim7_start(5000);
        } else
        // 上升沿,按键松开
            if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_SET) {
                key1_flag |= 0x1u << 1;
                // 定时器消抖动 5ms
                tim7_start(5000);
            }
    }
}

void TIM7_IRQHandler(void)
{
    //usart1_it_putchar('S');
    // 是更新中断
    if (TIM_GetITStatus(TIM7, TIM_IT_Update)) {
        // 关闭定时器
        tim7_end();
        // 开启 EXTI0 中断,按键中断
        EXTI->IMR |= 1;
        // 清除 TIM7 中断标志位
        TIM_ClearITPendingBit(TIM7, TIM_IT_Update);
        //usart1_it_putchar('E');
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 5.3 通用定时器

TIM2~TIM5 都是通用定时器

时基单元包含: ● 计数器寄存器(TIMx_CNT) ● 预分频器寄存器 (TIMx_PSC) ● 自动装载寄存器 (TIMx_ARR)

TIMx_ARR 用于将计数值(即周期)装载到 TIMx_CNT

TIMx_CNT 与 TIMx_CCRy 的值进行比较,用于 输出比较 和 PWM 模式。

PWM 输出时需要进行时基单元初始化和输出比较初始化。

主要特性

4 个时钟源

16位向上、向下、向上/向下自动装载计数器, 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65535之间的任意数值。

多达 4 个独立通道,可用于:

— 输入捕获 — 输出比较 — PWM 生成(边沿和中心对齐模式) — 单脉冲模式输出

使用外部信号控制定时器和定时器互连的同步电路。

如下事件发生时产生中断/DMA(6个独立的IRQ/DMA请求生成器):

— 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发) — 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数) — 输入捕获 — 输出比较

支持针对定位的增量(正交)编码器和霍尔传感器电路

触发输入作为外部时钟或者按周期的电流管理

通用定时器的应用

STM32 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等。

使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。STM32 的每个通用定时器都是完全独立的,没有互相共享的任何资源。

计数模式

通用定时器可以向上计数、向下计数、向上向下双向计数模式。

  1. 向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。
  2. 向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个计数器向下溢出事件。
  3. 中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。

时钟来源

定时器的时钟来源有 4 个:

  1. 内部时钟(CK_INT)
  2. 外部时钟模式 1:外部输入脚(TIx)
  3. 外部时钟模式 2:外部触发输入(ETR)
  4. 内部触发输入(ITRx) :使用 A 定时器作为 B 定时器的预分频器(A 为 B 提供时钟) 。

这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT时钟是从 APB1 倍频的来的,除非 APB1 的时钟分频数设置为 1(一般都不会是 1) ,否则通用定时器 TIMx 的时钟是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx的时钟就等于 APB1 的时钟。 这里还要注意的就是高级定时器的时钟不是来自 APB1,而是来自 APB2 的。

定时器中断配置

//使能定时器时钟。
RCC_APB1PeriphClockCmd();
//初始化定时器,配置ARR,PSC。
TIM_TimeBaseInit();
//开启定时器中断,配置NVIC。
NVIC_Init();
//使能定时器。
TIM_Cmd();
//编写中断服务函数。
TIMx_IRQHandler();
1
2
3
4
5
6
7
8
9
10

# 5.4 PWM 脉冲宽度调制

PWM是Pulse Width Modulation的缩写,意思是脉冲宽度调制,简称脉宽调制,是利用微处理器的数字 输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。 脉冲宽度调制的一个优点是从处理器到被控系统信号都是数字形式的,无需进行数模转换。

PWM是一种对模拟信号电平进行数字编码的方法,其根据相应载荷的变化来调制晶体管栅极或基极的偏置,来实现开关稳压电源输出晶体管或晶体管导通时间的改变,这种方式能使电源的输出电压在工作条件变化时保持恒定,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。通过高分辨率计数器的使用,脉冲宽度或方波的占空比被调制用来对一个具体模拟信号的电平进行编码。 PWM信号仍然是数字的,因为在给定的任何时刻,满幅值的直流供电要么完全有(ON),要么完全无(OFF)。电压或电流源是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用PWM进行编码。 多数负载(无论是电感性负载还是电容性负载)需要的调制频率高于10Hz,通常调制频率为1kHz到200kHz之间。

定时器时钟分频因子 ClockDivision 是决定数字滤波器采样频率的参数,之后在使用输入捕获滤波器时候 这些参数会被用到,可以根据硬件情况配置滤波。

CCMR2 寄存器的 OC3M[2:0] 配置输出比较模式:

110:PWM模式1- 在向上计数时,一旦TIMx_CNT< TIMx_CCR3时通道3为有效电平,否则为无效电平; 在向下计数时,一旦TIMx_CNT>TIMx_CCR3时通道3为无效电平(OC3REF=0),否则为有效电平(OC3REF=1)。

111:PWM模式2- 在向上计数时,一旦TIMx_CNT< TIMx_CCR3时通道3为无效电平,否则为有效电平; 在向下计数时,一旦TIMx_CNT>TIMx_CCR3时通道3为有效电平,否则为无效电平。

TIMx_CCER 中的 CC3P 决定有效电平为高还是低:

0:OC3高电平有效

1:OC3低电平有效

               CC3P  有效电平   TIMx_CNT<TIMx_CCR3  TIMx_CNT>=TIMx_CCR3

PWM模式1向上计数 0 高 高 低 1 低 低 高 PWM模式1向下计数 0 高 高 低 1 低 低 高 PWM模式2向上计数 0 高 低 高 1 低 高 低 PWM模式2向下计数 0 高 低 高 1 低 高 低

# 5.5 DMA 控制器

专门用搬运数据的片上外设

  1. 不需要CPU的干预可以实现数据的快速移动
  2. 字节(8bit) 半字(16bit) 全字(32bit)
  3. 实现内存到内存的数据搬运

DMA1 有 7 个通道,DMA2 有 5 个通道。

数据宽度要对齐。

共有四个软件优先级:很高、高、中等和低,在软件优先级相同时,采用硬件优先级:通道1优先于通道2,依此类推

可在内存和外设间传输,也可以在内存与内存间传输,但不能在外设和外设间传输。

可编程的数据传输数目:最大为65536。这里不是指字节数,而是以数据宽度为单位的数目。 如,数据宽度为 半字,则最大字节为 65536 * 2。

数据宽度:字节、半字、字。

注意修改 CNDTR 前必须关闭通道。

# 6 day 6

# 6.1 ADC

模拟电路:连续的

数字电路:离散的,电压:低电平、高电平、高阻态

ADC 是一个片上外设,可以 i/o 口的电压。

stm32RCt6 有 3 个 16通道12位ADC。

ADC 能将模拟量转化为数字量存在寄存器中,是一种逐次逼近型模拟数字转换器。

最后更新: 2023/9/12 08:24:09