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