参考
http://t.csdnimg.cn/P9H6x
一、sys文件夹介绍
在上述介绍的 sys 文件夹中,涉及了一些与系统控制、中断管理、低功耗模式、栈顶地址设置、系统时钟初始化以及缓存配置等相关的函数。以下是对每个功能的简要分析:
1.中断类函数:
sys_nvic_set_vector_table(): 设置中断向量表地址,中断向量表是一个包含中断服务程序入口地址的表。
sys_intx_enable(): 开启所有中断,通过设置相关标志位允许中断发生。
sys_intx_disable(): 关闭所有中断,通过清除相关标志位禁止中断发生,但不包括fault和NMI中断。
2.低功耗类函数:
sys_wfi_set(): 执行 WFI 指令,该指令使处理器进入等待低功耗状态,等待外部中断唤醒。
sys_standby(): 进入待机模式,是一种低功耗模式,主要特点是CPU停止运行。
sys_soft_reset(): 系统软复位,通过配置相关寄存器实现对整个系统的复位。
3.设置栈顶地址函数:
sys_msr_msp(): 设置栈顶地址,通过修改主堆栈指针(Main Stack Pointer,MSP)的值来设置栈顶地址。
4.系统时钟初始化函数:
sys_stm32_clock_init(): 设置系统时钟,这个函数通常在启动代码中调用,用于配置时钟源、PLL、分频等,确保系统时钟的正确初始化。
5.Cache 配置函数(F7/H7):
sys_cache_enable(): 使能 I-Cache 和 D-Cache,开启 D-Cache 强制透写。在某些处理器架构(如F7和H7)中,Cache的配置和使能可以提高程序的执行效率。
这些函数涵盖了系统的基本控制、中断管理、低功耗模式的配置、栈顶地址的设置、系统时钟的初始化以及缓存的配置等方面。在嵌入式系统中,这些功能对于系统的运行、时钟配置和低功耗管理非常重要。
二、deley文件夹介绍
2.1、deley文件夹函数简介
在 delay 文件夹中,涉及了一些延时函数,主要用于在嵌入式系统中实现微秒和毫秒级别的延时。以下是对每个功能的简要分析:
1.不使用OS:
在嵌入式系统中,有时候不使用操作系统(OS)是常见的情况,因为一些简单的应用可能不需要复杂的操作系统支持。
对于这种情况,提供了 delay_init() 函数,用于初始化系统滴答定时器。滴答定时器是一种在嵌入式系统中常用的计时器,用于生成精确定时的延时。
2.delay_us():
delay_us() 函数使用系统滴答定时器实现微秒级别的延时。通过读取滴答定时器的计数值,并进行一定的计算,实现对微秒级别的精确延时。
3.delay_ms():
delay_ms() 函数则是使用微秒级别的延时函数 delay_us() 实现毫秒级别的延时。通过在 delay_us() 的基础上进行倍乘,实现对毫秒级别的延时。
这些函数主要用于在嵌入式系统中提供简单的延时功能。在没有操作系统支持的情况下,使用滴答定时器是一种常见的方式来实现延时。这对于需要进行时间控制的嵌入式应用,例如控制器的初始化、通信时序等,都是有用的。在使用这些函数时,需要注意系统的时钟配置,以确保延时的准确性。
2.2、SysTick工作原理
SysTick 是 ARM Cortex-M 系列微控制器内置的一个系统定时器。下面对 SysTick 的工作原理进行分析:
1.递减计数器:
SysTick 包含一个 24 位的递减计数器。这意味着计数器的值从加载的初始值开始递减,直到达到零。
递减计数器的宽度决定了 SysTick 可以提供的最大定时周期。
2.时钟源:
SysTick 的时钟源可以是 HCLK(对于 F1/F4/F7 等系列)或 SYS_d1cpre_ck(对于 H7 等系列),具体取决于芯片型号和配置。
HCLK 是核心时钟,而 SYS_d1cpre_ck 是 H7 系列中的一个时钟源。
3.分频器:
时钟源可以通过一个可编程分频器进行分频,以确定 SysTick 计数器的工作频率。
分频可以用于调整 SysTick 的计数速率,使其适应不同的应用场景。
4.重装载值(LOAD):
SysTick 包含一个可以设置的重装载值 LOAD。当递减计数器达到零时,它可以自动重新加载为 LOAD 的值。
这就意味着 SysTick 在每次计数完成后可以自动重新开始新一轮的递减计数。
5.计数完成标志(COUNTFLAG):
当递减计数器的值达到零时,会产生一个计数完成标志 COUNTFLAG。
软件可以检查这个标志来判断是否达到了设定的定时周期。
6.递减计数和重装载机制:
SysTick 的基本工作机制是递减计数,每次计数完成都会检查 COUNTFLAG。
当 COUNTFLAG 为 1 时,表示计数完成,可以执行相应的操作。然后递减计数器被重新加载为 LOAD。
总体来说,SysTick 是一种简单而强大的定时器,适用于许多嵌入式系统中的定时和延时操作。通过调整时钟源、分频因子和重装载值,可以实现不同精度和范围的定时需求。SysTick 经常被用于创建精确的延时函数、定时任务等。
2.3、SysTick寄存器介绍
SysTick 寄存器包括 SysTick 控制及状态寄存器(CTRL)、SysTick 重装载数值寄存器(LOAD)和 SysTick 当前数值寄存器(VAL)。以下是对每个寄存器的简要分析:
1.SysTick 控制及状态寄存器 (CTRL):
CTRL 寄存器是 SysTick 的控制和状态寄存器,用于配置 SysTick 的工作方式和获取状态信息。
CTRL 寄存器的位字段包括:
Bit[0] - ENABLE:用于启用(1)或禁用(0)SysTick 计数器。
Bit[1] - TICKINT:用于启用(1)或禁用(0)SysTick 定时中断。
Bit[2] - CLKSOURCE:用于选择时钟源,0 表示外部时钟源(通常是处理器时钟),1 表示处理器时钟(通常是时钟源除以 8)。
Bit[16] - COUNTFLAG:表示 SysTick 计数器是否归零,当计数器值变为 0 时,该位会置 1。
2.SysTick 重装载数值寄存器 (LOAD):
LOAD 寄存器用于设置 SysTick 的重装载值,即计数器递减到 0 后自动重新加载的值。
当 CTRL 寄存器的 ENABLE 位被置 1 时,LOAD 寄存器的值会被加载到 SysTick 计数器,从而决定了 SysTick 的定时周期。
3.SysTick 当前数值寄存器 (VAL):
VAL 寄存器用于读取当前的 SysTick 计数器的值。
当 CTRL 寄存器的 ENABLE 位被置 1 时,VAL 寄存器的值表示当前剩余的计数器值,可以通过读取该寄存器来获取 SysTick 的当前计数状态。
这三个寄存器协同工作,实现了 SysTick 定时器的基本功能。CTRL 寄存器用于配置 SysTick 的工作方式,LOAD 寄存器用于设置定时周期,而 VAL 寄存器用于读取当前计数器的值。通过这些寄存器的设置和读取,可以对 SysTick 进行灵活的配置和使用。
2.4、delay_init()函数
用于初始化延时函数的 delay_init() 函数。以下是对这个函数的简要分析:
void delay_init(uint16_t sysclk)
{SysTick->CTRL = 0;HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);g_fac_us = sysclk / 8;
}
1.SysTick->CTRL = 0;:
将 SysTick 控制寄存器 (CTRL) 的值设为 0,即清零。这是为了确保在初始化之前 SysTick 定时器被禁用。
2.HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);:
通过调用 HAL_SYSTICK_CLKSourceConfig 函数配置 SysTick 的时钟源。在这里,时钟源被配置为 HCLK(处理器时钟)除以 8。
这里选择 HCLK 除以 8 作为 SysTick 的时钟源,这是为了降低 SysTick 的时钟频率,从而延长计数器的溢出周期,提高延时的范围。
3.g_fac_us = sysclk / 8;:
计算一个全局变量 g_fac_us 的值,该变量用于后续的延时计算。
sysclk / 8 表示实际的 SysTick 时钟频率,这个值将用于后续的微秒级延时计算。
该函数的主要目的是对 SysTick 定时器进行初始化配置,包括清零控制寄存器、配置时钟源为 HCLK 除以 8,以及计算用于延时的全局变量的值。这样,通过调用 delay_us() 和 delay_ms() 函数,可以实现微秒级和毫秒级的延时。
2.5、delay_us()函数
用于实现微秒级延时的 delay_us() 函数。以下是对这个函数的简要分析:
void delay_us(uint32_t nus)
{uint32_t temp;SysTick->LOAD = nus * g_fac_us; /* 设置倒计时的时间,即加载延时的微秒数 */SysTick->VAL = 0x00; /* 清空计数器 */SysTick->CTRL |= 1 << 0; /* 开始倒计时 */do{temp = SysTick->CTRL;} while ((temp & 0x01) && !(temp & (1 << 16))); /* 等待计时完成 */SysTick->CTRL &= ~(1 << 0); /* 关闭 SysTick 定时器 */SysTick->VAL = 0X00; /* 清空计数器 */
}
1.SysTick->LOAD = nus * g_fac_us;:
将 SysTick->LOAD 寄存器的值设置为延时的微秒数乘以全局变量 g_fac_us。这个值决定了 SysTick 的计时周期,即延时的实际时间。
2.SysTick->VAL = 0x00;:
清空 SysTick 的当前计数器值,确保计数器从零开始。
3.SysTick->CTRL |= 1 << 0;:
设置 SysTick->CTRL 寄存器的 ENABLE 位(位 0)为 1,开始倒计时。
4.do…while 循环:
在循环中,使用 temp = SysTick->CTRL 不断读取 SysTick->CTRL 寄存器的值,判断条件是 temp & 0x01 为 1(即 ENABLE 位为 1),同时 temp & (1 << 16) 为 0(即倒计时未结束)。
循环等待计时完成,当计时完成时,temp & 0x01 为 0,循环退出。
5.SysTick->CTRL &= ~(1 << 0);:
在倒计时完成后,将 SysTick->CTRL 寄存器的 ENABLE 位清零,关闭 SysTick 定时器。
6.SysTick->VAL = 0X00;:
最后,再次清空 SysTick 的当前计数器值。
该函数的主要目的是通过 SysTick 定时器实现微秒级的延时。在 SysTick->LOAD 寄存器中设置适当的值,然后通过控制 SysTick->CTRL 寄存器的 ENABLE 位实现定时器的启动和关闭。循环等待计时完成以保证准确的延时。
2.6、delay_ms()函数
用于实现毫秒级延时的 delay_ms() 函数。以下是对这个函数的简要分析:
void delay_ms(uint16_t nms)
{uint32_t repeat = nms / 1000; /* 计算整秒数,每秒使用 delay_us 实现 1000 毫秒延时 */uint32_t remain = nms % 1000; /* 计算余数,即剩余的毫秒数 */while (repeat){delay_us(1000 * 1000); /* 利用 delay_us 函数实现 1000 毫秒延时 */repeat--;}if (remain){delay_us(remain * 1000); /* 利用 delay_us 函数,把尾数延时(remain 毫秒)给做了 */}
}
1.repeat = nms / 1000;:
计算整秒数,即通过将毫秒数除以 1000 得到整数部分。这里采用整数除法,以得到需要通过 delay_us 实现的整秒数。
2.remain = nms % 1000;:
计算余数,即剩余的毫秒数。这是因为可能存在不足 1000 毫秒的部分,需要通过 delay_us 函数进行延时。
3.while (repeat):
使用循环,通过 delay_us(1000 * 1000) 实现整秒数的延时。这是因为 delay_us 函数实现的是微秒级延时,因此需要乘以 1000 得到毫秒级延时。
4.if (remain):
判断是否存在不足 1000 毫秒的剩余部分,如果存在,则通过 delay_us(remain * 1000) 实现剩余毫秒数的延时。
该函数的主要目的是通过调用 delay_us() 函数实现毫秒级的延时。通过循环整秒数的部分和处理剩余毫秒数的部分,实现了毫秒级的延时功能。这样,在整数秒数的延时和剩余毫秒数的延时之间,可以实现相对较为准确的毫秒级延时。
三、USART 文件夹介绍
在嵌入式系统中,usart 文件夹通常包含与 USART(通用同步异步收发器)通信相关的函数和文件。以下是可能在 usart 文件夹中找到的一些常见文件和功能的简要介绍:
1.USART 配置文件(例如 usart_config.h):
这个文件可能包含 USART 相关的宏定义、常量和配置参数。通常,你可以在这里设置 USART 的波特率、数据位、停止位等参数。
2.USART 初始化函数(例如 usart_init.c/.h):
提供 USART 的初始化函数,用于配置 USART 模块并启用相应的硬件资源。这通常包括对寄存器的设置以及时钟和引脚的配置。
3.USART 发送函数(例如 usart_send.c/.h):
包含用于向 USART 发送数据的函数。这些函数通常涉及到数据缓冲区的管理、数据格式的处理以及通过 USART 寄存器的写操作来发送数据。
4.USART 接收函数(例如 usart_receive.c/.h):
包含用于从 USART 接收数据的函数。这些函数通常涉及到数据缓冲区的管理、接收中断的处理以及通过 USART 寄存器的读操作来接收数据。
5.USART 中断处理函数(例如 usart_interrupt.c/.h):
提供用于处理 USART 中断的函数。这些函数通常包括处理接收和发送中断,并根据需要执行相应的操作。
6.USART 外设驱动(例如 usart_driver.c/.h):
可能包含更高层次的函数,用于处理 USART 的一些常见操作,如发送字符串、接收字符串等。
7.USART 示例应用程序(例如 usart_example.c):
提供一些示例代码,演示如何在具体的应用中使用 USART。这可以作为用户的参考,帮助理解和使用 USART 相关功能。
这些文件通常组成了一个完整的 USART 驱动库,为用户提供了方便的接口,使其能够轻松地在应用程序中集成和使用 USART 通信。具体的文件结构和功能可能因不同的开发环境和硬件平台而有所不同。
3.1、printf函数输出流程
printf函数输出流程如下:
1.用户调用 printf() 函数:
在程序中,用户调用 printf() 函数来格式化输出字符串到标准输出流。
2.C 标准库(printf 部分):
printf() 函数属于 C 标准库(stdio.h 头文件)。该头文件中包含了一系列用于输入输出操作的标准函数,其中就包括 printf。
3.printf 函数由编译器提供的 stdio.h 解析:
在编译阶段,编译器会将 printf 函数解析为与标准输出流相关的一系列函数调用。
4.标准库的输出函数(如 fputc()):
在 printf 的内部,它会调用标准库的输出函数,比如 fputc。
fputc 是一个通用的字符输出函数,其功能是将一个字符写入指定的输出流(例如标准输出流)。
5.用户根据最终输出的硬件重新定义输出函数:printf 重定向:
最终的输出是通过底层的输出函数实现的,比如 fputc。用户可以根据最终输出的硬件重新定义这些函数,这个过程被称为 “printf 重定向”。
通过重新定义输出函数,用户可以将输出重定向到不同的设备,比如串口、文件等。这对于嵌入式系统和特殊硬件平台很有用。
总体而言,printf 函数是一个高层次的接口,它在底层调用标准库的输出函数,而用户可以通过重定向这些输出函数来适应不同的硬件或输出设备。这种机制允许程序员将标准的输入输出函数与特定硬件进行适配,提高了代码的可移植性。
3.2、printf的使用
printf 是 C 语言标准库中用于格式化输出的函数。下面是 printf 的基本用法和如何输出特殊字符的方法:
1.输出字符串:
printf(“字符串\r\n”);:直接输出指定的字符串,\r\n 表示回车和换行,通常用于换行输出。
2.使用输出控制符和输出参数:
printf(“输出控制符”, 输出参数);:通过输出控制符指定输出参数的格式,例如 %d 表示输出整数。
printf(“输出控制符1 输出控制符2 …”, 输出参数1, 输出参数2, …);:可以同时输出多个参数,并通过多个输出控制符指定它们的格式。
3.混合输出非输出控制符和输出控制符:
printf(“非输出控制符 输出控制符 非输出控制符”, 输出参数);:可以混合输出非输出控制符和输出控制符,只有输出控制符会影响参数的格式。
4.输出特殊字符 %、\ 和双引号:
若要在 printf 中输出特殊字符 %、\ 或双引号,需要使用转义字符 \:
%:printf(“输出 %% 字符”);
\:printf(“输出 \ 字符”);
双引号:printf(“输出 " 字符”);
#include <stdio.h>int main() {// 示例1:输出字符串printf("Hello, World!\r\n");// 示例2:使用输出控制符和输出参数int num = 42;printf("The number is %d\r\n", num);// 示例3:混合输出非输出控制符和输出控制符char ch = 'A';printf("Character: %c, ASCII code: %d\r\n", ch, ch);// 示例4:输出特殊字符 %、\ 和双引号printf("Output %% character: %%\r\n");printf("Output \\ character: \\\r\n");printf("Output \" character: \"\r\n");return 0;
}
3.3、printf函数支持
1.避免使用半主机模式:
半主机模式是指将 printf 函数的输出重定向到开发环境的终端窗口,通常通过串口或仿真器实现。在嵌入式系统中,由于没有终端窗口,半主机模式可能导致编译后的代码过大,因此需要避免使用半主机模式。
两种常见的方法:
微库法:
在编译时使用 -specs=nano.specs 选项,该选项会使用微库(nano.specs)来减小代码大小。
示例编译命令:arm-none-eabi-gcc -o output.elf input.c -specs=nano.specs
代码法:
在代码中添加如下语句,关闭缓冲区并禁用半主机模式:
setvbuf(stdout, NULL, _IONBF, 0);
2. 实现 fputc 函数:
在嵌入式系统中,需要用户自己实现 fputc 函数,以便 printf 函数正确输出字符。下面是一个简单的例子:
#include <stdio.h>// 实现 fputc 函数
int fputc(int c, FILE *stream) {// 在这里实现将字符 c 输出到相应的硬件设备,例如串口// 例如,如果使用串口输出,可以使用类似于 UART_SendChar(c) 的函数// 返回输出的字符或者 EOF(表示错误)// 这里仅为示例,没有具体的输出操作return c;
}int main() {// 避免使用半主机模式的两种方法// 方法1:微库法// arm-none-eabi-gcc -o output.elf input.c -specs=nano.specs// 方法2:代码法setvbuf(stdout, NULL, _IONBF, 0);// 使用 printf 函数printf("Hello, World!\n");return 0;
}
在上述代码中,fputc 函数用于实现字符的输出操作,具体的输出动作应该根据实际的硬件设备进行实现。main 函数中使用 printf 输出字符串,而通过设置缓冲区为无缓冲 (_IONBF),可以避免缓冲区的影响。
半主机模式简介
半主机模式是一种在嵌入式系统中用于将输入/输出(I/O)请求传送到运行调试器的主机的机制。这种模式通常通过调试工具、仿真器或者开发板连接到主机来实现。半主机模式的目的是方便在嵌入式系统中进行调试和开发。
在半主机模式中,开发者可以在嵌入式系统中使用标准的输入输出函数(如printf和scanf)进行调试。这些函数的输出被传送到主机,而主机上的终端模拟器则模拟了嵌入式系统的输入和输出。这使得在嵌入式系统中进行调试时能够使用类似于在主机上调试的方式。
然而,半主机模式在一些嵌入式系统开发中可能存在一些问题和限制,例如:
1.代码大小: 启用半主机模式可能导致生成的代码变得较大,因为需要包含一些用于与主机通信的额外代码。
2.实时性: 半主机模式可能引入一些额外的延迟,特别是在需要频繁进行输入输出的应用中。
3.硬件依赖性: 半主机模式通常依赖于特定的仿真器或调试工具,因此在更换硬件平台时可能需要重新调整。
正如你所提到的,一般情况下,在嵌入式系统的实际应用中,不使用半主机模式是一个较为常见的选择。在实际产品中,通常会使用其他手段,如串口通信或者其他特定的调试接口来进行调试和信息输出,以避免半主机模式可能引入的一些问题。
方法一:微库法
方法二:代码法
1.#pragma import(__use_no_semihosting):
这个 #pragma 指令用于告诉编译器不要使用半主机函数。半主机函数通常用于与调试器进行交互,包括输入输出操作。通过使用这个 #pragma,你告诉编译器在编译时不要链接半主机函数,从而避免在嵌入式系统中引入不必要的代码。
2.定义 __FILE 结构体:
__FILE 是一个预定义的宏,用于在编译时传递当前源文件的名称。在某些情况下,特别是使用 HAL 库时,可能会遇到对 __FILE 结构体的要求。通过定义 __FILE 结构体,你确保了在编译时正确传递源文件的信息。
3.定义 FILE __stdout:
在使用 printf 函数等输出函数时,通常需要定义一个输出流,这个输出流可以是标准输出流。通过定义 FILE __stdout,你为 printf 函数提供了一个输出流,从而使得输出函数知道要输出到哪里。
4.实现 _ttywrch、_sys_exit 和 _sys_command_string:
这三个函数通常是与半主机相关的函数,用于处理底层的输入输出和系统命令。在没有使用半主机模式的情况下,你可能需要提供这些函数的实现,以满足编译器的链接要求。具体的实现可能根据你的嵌入式系统和开发环境而有所不同。
在使用 AC5 和 AC6 时,确保正确定义和实现这些关键的结构体和函数,以满足 HAL 库和编译器的要求,同时避免引入不必要的半主机函数。这样可以确保在嵌入式系统中编写和调试代码时,不会受到半主机模式可能带来的一些问题。
实现fputc函数
实现 fputc 函数,该函数用于将单个字符发送到 USART(串口)。下面是代码的简要分析:
#define USART_UX USART1/* 重定义 fputc 函数, printf 函数最终会通过调用 fputc 输出字符串到串口 */
int fputc(int ch, FILE *f) {while ((USART_UX->SR & 0X40) == 0); /* 等待上一个字符发送完成 */USART_UX->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到 DR 寄存器 */return ch;
}
1.#define USART_UX USART1:
定义了一个宏 USART_UX,表示使用的 USART 模块,这里定义为 USART1。你可以根据实际硬件连接选择合适的 USART 模块。
*2.int fputc(int ch, FILE f) {…}:
实现了 fputc 函数,该函数会被 printf 调用来将字符输出到串口。
int ch 是要输出的字符。
FILE *f 是文件指针,由于 fputc 函数是标准库函数,所以需要有这个参数,但实际上在这个实现中没有使用。
3.while ((USART_UX->SR & 0X40) == 0);:
这是一个忙等待循环,用于等待上一个字符发送完成。USART_UX->SR 表示 USART 状态寄存器,0X40 表示 USART 的发送缓冲区空标志位(TXE)。
4.USART_UX->DR = (uint8_t)ch;:
将要发送的字符 ch 写入 USART 数据寄存器(DR)。USART 数据寄存器用于存储要发送或接收的数据。
5.return ch;:
返回发送的字符。在 printf 中,该返回值并不会被使用,因此可以简单地返回发送的字符。
这段代码实际上是一个典型的 USART 发送字符的实现,用于在嵌入式系统中通过串口输出。确保 USART 的初始化和配置在此代码之前完成,以便正确地将字符发送到 USART。