5.4 Cortex-M4F 使用说明
本章说明 N32H49x 的 Cortex-M4F 内核相关功能的使用,包括位段(BitBand)访问、浮点单元(FPU)浮点运算以及线程模式的特权级别切换,并说明本章示例所依赖的接口来源。Cortex-M4F 是处理器内核,SDK 不提供标准外设驱动那样的专有驱动文件,其接口来自 CMSIS Core 头文件与器件启动文件,请配合用户手册的“系统(System)”章节阅读。
5.4.1 相关文件
| 相对路径 | 作用 |
|---|---|
firmware/CMSIS/core/ |
CMSIS Core 头文件目录:core_cm4.h 与 cmsis_armcc.h、cmsis_gcc.h 等编译器适配头,以及 arm_math.h、mpu_armv7.h |
firmware/CMSIS/device/ |
器件定义与启动文件:n32h49x.h(含中断号枚举 IRQn_Type)、system_n32h49x.c、startup/ 下的 startup_n32h49x.s 等 |
projects/n32h49x_EVAL/examples/Cortex-M4F/ |
本章示例根目录,含 M4F_BitBand/、M4F_FPU/、M4F_Mode_Privilege/ 3 个示例 |
projects/n32h49x_EVAL/examples/Cortex-M4F/M4F_BitBand/ |
示例工程目录组成:MDK-ARM/、EWARM/、inc/、src/、readme.txt |
projects/n32h49x_EVAL/bsp/ |
该示例使用的板级支持文件:日志初始化与打印串口重映射 |
5.4.2 API 参考
Cortex-M4F 是处理器内核,SDK 中没有标准外设驱动那样的 n32h49x_xxx.h/.c 驱动源文件,其接口来自 CMSIS Core 头文件与器件启动文件:
firmware/CMSIS/core/:CMSIS Core 头文件与各编译器的适配头(cmsis_armcc.h、cmsis_armclang.h、cmsis_gcc.h、cmsis_iccarm.h),提供内联函数__get_CONTROL/__set_CONTROL、__get_PSP/__set_PSP、__get_MSP/__set_MSP、__get_IPSR、__get_PRIMASK/__set_PRIMASK、__get_FPSCR/__set_FPSCR,以及内核指令封装__DSB、__ISB、__WFI、__enable_irq、__disable_irq。firmware/CMSIS/core/core_cm4.h:内核外设定义(SCB、SysTick、NVIC)与特性宏__FPU_USED。firmware/CMSIS/device/n32h49x.h:器件寄存器定义与中断号枚举IRQn_Type。firmware/CMSIS/device/startup/startup_n32h49x.s:向量表与异常、中断入口Reset_Handler、NMI_Handler、HardFault_Handler、MemManage_Handler、BusFault_Handler、UsageFault_Handler、SVC_Handler、DebugMon_Handler、PendSV_Handler、SysTick_Handler。
本章示例用到的接口集中在堆栈与特权控制:__set_PSP 把进程栈顶写入 PSP、__set_CONTROL 按 CONTROL 寄存器在过程栈与特权位之间切换;触发 SVCall 的 __SVC 并非 CMSIS 接口,而是示例按编译器自行定义的内联函数。
5.4.3 Demo 示例说明
本节说明 Cortex-M4F 的 3 个示例,分别演示位段访问、浮点单元运算,以及线程模式的特权级别切换。
5.4.3.1 M4F_BitBand 位段访问
工程目录: projects/n32h49x_EVAL/examples/Cortex-M4F/M4F_BitBand/
初始化日志串口并打印示例名→记录变量 Data 的地址并算出 SRAM 位段别名基址→循环把变量 Data 的第 i 位置 1 并读回打印→再循环把第 i 位清 0 并读回打印→8 位遍历完成后进入空主循环
| 项目 | 源码解读 |
|---|---|
| 功能 | 通过位段别名地址读写变量的单个比特; 把变量 Data 的第 i 位依次置 1 与清 0,每次读取整个变量并打印。 |
| 源码组成 | src/main.c:main 与位段读写宏 Data_SetBit_BB、Data_ResetBit_BB、Data_GetBit_BB;inc/main.h、inc/n32h49x_it.h:公共声明与中断头文件;src/n32h49x_it.c:异常与中断处理函数;readme.txt:功能、环境与使用说明;MDK-ARM/、EWARM/:Keil 与 IAR 工程。 |
| 关键配置 | 系统时钟为 HSI+PLL,240 MHz; 打印串口为 PA9,波特率 115200; 位段别名基址取 SRAM 别名基址 0x22000000,被访问变量位于 SRAM。 |
| 程序与数据路径 | main 调用 log_init 并打印示例名后,把变量 Data 的地址存入 DataAddr;循环对第 i 位调用置位宏打印一次,再调用复位宏打印一次,读取 Data 得到整字结果。 |
| 主要 API | Data_SetBit_BB、Data_ResetBit_BB、Data_GetBit_BB、log_init |
| 验证方法 | 复位运行后串口打印 Cortex-M4F BitBand;随后 8 次打印置位结果,从 bit 0 读到 0x01 依次到 bit 7 读到 0x80; 再 8 次打印清 0 结果,均读到 0x00。 |
| 使用提示 | 宏把目标位地址换算成别名地址,字节偏移左移 5 位等价于乘以 32,位号左移 2 位等价于乘以 4; 写别名区一个字即对目标位做原子读-改-写。 开发环境为 KEIL MDK-ARM V5.34 或 IAR EWARM 8.50.1。 |
核心代码解读与移植
示例用 SRAM 位段别名把变量的一个比特映射成一个可读写的字,从而用普通整字赋值原子地操作单个位。
数据路径: main 记录变量 Data 的地址 → 由别名基址、字节偏移与位号算出别名地址 → 写别名区一个字置位并读回整字 → 再写别名区一个字清位并读回 → 遍历 0 到 7 位后进入空主循环
1. 取位段区与别名区的基址。 SRAM 位段区从 0x20000000 开始,对应的别名区从 0x22000000 开始,两个基址都先定义成宏。
/* RAM address */
#define RAM_BASE ((uint32_t)0x20000000)
/* RAM bitband address */
#define RAM_BB_BASE ((uint32_t)0x22000000)
2. 按映射公式由字节偏移与位号算出别名地址。 源文件用一段注释给出映射公式:别名字节地址等于别名区基址加上字节偏移乘以 32,再加上位号乘以 4。宏里的 << 5 就是乘以 32,<< 2 就是乘以 4。
/* A mapping formula shows how to reference each word in the alias region to a
corresponding bit in the bit-band region. The mapping formula is:
bit_byte_addr = bit_band_base + (byte_offset x 32) + (bit_number x 4)
Parameter Description:
bit_byte_addr: The address of the byte in the alias memory region that maps to the targeted bit.
bit_band_base: The starting address of the alias region
byte_offset: The number of the byte in the bit-band region that contains the targeted bit
bit_number: The bit position (0-7) of the targeted bit */
3. 读一位用右移取位。 读操作不走别名区,直接把变量右移到位号处取最低位即可。
#define Data_GetBit_BB(DataAddr, BitNumber) (Data >> BitNumber)
4. 循环用别名区原子置位与清位。 每轮先把第 i 位置 1 并打印整字,再把第 i 位清 0 并打印整字,逐位验证别名写只影响目标位。
for (i = 0; i < 8; i++)
{
/* Modify Data variable bit i */
Data_SetBit_BB(DataAddr, i); /* Data = (0x00000001 << i) */
printf("Set bit %d of variable Data to 1 = 0x%02X \r\n", i, Data);
Data_ResetBit_BB(DataAddr, i); /* Data = 0x00000000 */
printf("Set bit %d of variable Data to 0 = 0x%02X \r\n", i, Data);
}
对应源码: src/main.c
5.4.3.2 M4F_FPU 浮点单元
工程目录: projects/n32h49x_EVAL/examples/Cortex-M4F/M4F_FPU/
初始化日志串口与计时用的通用定时器→用缩放系数在缓冲区里逐点迭代计算 Julia 集→计算前后读取定时器计数得到耗时→按编译期开关打印 FPU On 或 FPU Off 及耗时→打印完成后进入空主循环
| 项目 | 源码解读 |
|---|---|
| 功能 | 演示 Cortex-M4F 浮点单元对同一段浮点运算的加速效果; 用 Julia 集迭代做基准,测出开启与关闭 FPU 时的耗时并打印对比。 |
| 源码组成 | src/main.c:main 与 InitTIMER、StartTIMER、StopTIMER;src/julia_fpu.c:浮点密集的 GenerateJulia_fpu;inc/julia_fpu.h:迭代常数与函数声明;inc/main.h、inc/n32h49x_it.h:公共声明与中断头文件;readme.txt、MDK-ARM/、EWARM/:使用说明与 Keil、IAR 工程。 |
| 关键配置 | 系统时钟为 HSI+PLL,240 MHz; 打印串口为 PA9,波特率 115200; 计时用 GTIM3,预分频为 HclkFreq/50000,周期 65535; 屏幕缓冲区为 100 x 50 字节,缩放表 26 项。 |
| 程序与数据路径 | main 调用 log_init 后调用 InitTIMER 配置 GTIM3;随后调用 StartTIMER 清零并启动计数,执行 GenerateJulia_fpu,再用 StopTIMER 读取计数值,除以 20 换算成毫秒后打印。 |
| 主要 API | InitTIMER、StartTIMER、StopTIMER、GenerateJulia_fpu、log_init |
| 验证方法 | 复位运行后串口打印 Cortex-M4F FPU;随后打印 FPU On : xxms 或 FPU Off : xxms,标签由编译期 __FPU_USED 决定;同一段浮点运算在开启 FPU 时耗时明显更短。 |
| 使用提示 | 在 Keil 的 Options for Target 的 Target 页用 Floating Point Hardware 选择开启或关闭 FPU,重编译后再对比打印的耗时;关闭 FPU 时浮点运算改由软件浮点库完成。 开发环境为 KEIL MDK-ARM V5.34 或 IAR EWARM 8.50.1。 |
核心代码解读与移植
示例用 Julia 集迭代做一段浮点密集运算,并用通用定时器测出同一段运算在开启与关闭 FPU 时的执行时间。
数据路径: main 清屏并 InitTIMER 配置 GTIM3 → StartTIMER 清零并启动计数 → GenerateJulia_fpu 做浮点迭代并写入缓冲区 → StopTIMER 读取计数得到耗时 → 除以 20 换算成毫秒后打印
1. 用编译期开关区分 FPU 状态。 __FPU_USED 由编译器按目标设置给出,示例用它选择打印的标签,硬件浮点与软件浮点用同一份代码。
#if (__FPU_USED == 1)
#define SCORE_FPU_MODE "FPU On"
#define PROGRESS_FPU_MODE "FPU ON"
#else
#define SCORE_FPU_MODE "FPU Off"
#define PROGRESS_FPU_MODE "FPU OFF"
#endif /* __FPU_USED */
2. 用通用定时器测出运算耗时。 预分频把计数频率定在 50 kHz,周期取最大值,测量前清零并启动计数。
void InitTIMER(void)
{
RCC_ClocksType RCC_Clocks;
/* GPTIM3 clock enable */
RCC_EnableAPB1PeriphClk(RCC_APB1_PERIPHEN_GTIM3,ENABLE);
RCC_GetClocksFreqValue(&RCC_Clocks);
/* DeInit GTIM3 */
TIM_DeInit(GTIM3);
TIM_InitTimBaseStruct(&TIM_TimeBaseStructure);
/* Time Base configuration */
TIM_TimeBaseStructure.Prescaler = ((RCC_Clocks.HclkFreq / 50000));
TIM_TimeBaseStructure.CounterMode = TIM_CNT_MODE_UP;
TIM_TimeBaseStructure.Period = 65535;
TIM_TimeBaseStructure.ClkDiv = TIM_CLK_DIV1;
TIM_InitTimeBase(GTIM3, &TIM_TimeBaseStructure);
}
3. 浮点迭代写入缓冲区。 Julia 集的双层循环里每一轮都做若干次单精度乘加,关闭 FPU 时这些运算改由软件浮点库完成,因而明显更慢。
while ((i < ITERATION - 1) && (rayon < 4))
{
tmp1 = num_real * num_real;
tmp2 = num_img * num_img;
num_img = 2 * num_real * num_img + IMG_CONSTANT;
num_real = tmp1 - tmp2 + REAL_CONSTANT;
rayon = tmp1 + tmp2;
i++;
}
/* Store the value in the buffer */
buffer[x + y * size_x] = i;
4. 计数换算成毫秒后打印。 计数频率为 50 kHz,因此把计数值除以 20 就得到毫秒数,标签与耗时一起打印。
/* Get elapsed time */
score_fpu = StopTIMER();
printf("%s : %dms\r\n", SCORE_FPU_MODE, score_fpu/20);
break;
对应源码: src/main.c、src/julia_fpu.c
5.4.3.3 M4F_Mode_Privilege 特权模式切换
工程目录: projects/n32h49x_EVAL/examples/Cortex-M4F/M4F_Mode_Privilege/
初始化日志串口并清零进程栈内存→把进程栈顶写入 PSP 并选择进程栈→在线程模式下切到非特权访问并检查是否成功→通过 SVC 异常在异常模式下切回特权访问→打印两次切换结果后进入空主循环
| 项目 | 源码解读 |
|---|---|
| 功能 | 演示线程模式的堆栈与特权级别切换; 先把线程模式堆栈从主栈切到进程栈,再切到非特权访问,最后通过 SVC 异常在异常模式下切回特权访问并打印结果。 |
| 源码组成 | src/main.c:main 与 __SVC、堆栈及特权宏定义;src/n32h49x_it.c:SVC_Handler 与各异常处理函数;inc/main.h、inc/n32h49x_it.h:公共声明与中断头文件;readme.txt:功能、环境与使用说明;MDK-ARM/、EWARM/:Keil 与 IAR 工程。 |
| 关键配置 | 系统时钟为 HSI+PLL,240 MHz; 打印串口为 PA9,波特率 115200; 进程栈数组大小为 0x200 字节,线程模式初始使用主栈。 |
| 程序与数据路径 | main 调用 log_init 后清零进程栈数组,用 __set_PSP 把进程栈顶写入 PSP,再用 __set_CONTROL 选择进程栈;接着把线程模式切到非特权并读回 CONTROL 判断,随后调用 __SVC 触发异常,由 SVC_Handler 切回特权。 |
| 主要 API | __set_PSP、__get_PSP、__set_CONTROL、__get_CONTROL、__SVC、SVC_Handler |
| 验证方法 | 复位运行后串口打印 Cortex-M4F Mode Privilege;随后打印 Cortex-M4F enter Unprivileged Mode OK 与 Cortex-M4F enter Privileged Mode OK;也可在调试器的 Cortex 寄存器窗口查看 CONTROL 与 PSP 的值。 |
| 使用提示 | 线程模式本身不能把非特权切回特权,必须在异常模式下改 CONTROL,示例正是用 SVC 进入 SVC_Handler 完成回切;非特权访问主要限制某些 MSR 字段以及对 NVIC、SysTick 等系统控制空间寄存器的访问。开发环境为 KEIL MDK-ARM V5.34 或 IAR EWARM 8.50.1。 |
核心代码解读与移植
示例演示线程模式在主栈与进程栈之间切换、在特权与非特权之间切换,以及如何借 SVC 异常在异常模式下切回特权。
数据路径: main 清零进程栈数组 → __set_PSP 写入进程栈顶并用 __set_CONTROL 选择进程栈 → __set_CONTROL 打开非特权位并读回检查 → __SVC 触发 SVCall → SVC_Handler 清 CONTROL 切回特权
1. 为线程模式准备进程栈。 先把保留的数组清零,再把栈顶(数组末尾)写入 PSP,最后用 CONTROL 选择进程栈。
/* Switch Thread mode Stack from Main to Process */
/* Initialize memory reserved for Process Stack */
for (Index = 0; Index < SP_PROCESS_SIZE; Index++)
{
PSPMemAlloc[Index] = 0x00;
}
/* Set Process stack value */
__set_PSP((uint32_t)PSPMemAlloc + SP_PROCESS_SIZE);
/* Select Process Stack as Thread mode Stack */
__set_CONTROL(SP_PROCESS);
2. 切到非特权并读回 CONTROL 检查。 置 CONTROL 的位 0 进入非特权访问,再读回该位判断是否生效。
/* Switch Thread mode from privileged to unprivileged */
/* Thread mode has unprivileged access */
__set_CONTROL(THREAD_MODE_UNPRIVILEGED | SP_PROCESS);
/* Unprivileged access mainly affect ability to:
- Use or not use certain instructions such as MSR fields
- Access System Control Space (SCS) registers such as NVIC and SysTick */
/* Check Thread mode privilege status */
if ((__get_CONTROL() & 0x01) == THREAD_MODE_PRIVILEGED)
{
/* Thread mode has privileged access */
ThreadMode = THREAD_MODE_PRIVILEGED;
printf("Cortex-M4F enter Unprivileged Mode Fail \r\n");
}
else
{
/* Thread mode has unprivileged access*/
ThreadMode = THREAD_MODE_UNPRIVILEGED;
printf("Cortex-M4F enter Unprivileged Mode OK \r\n");
}
3. 按编译器定义触发 SVCall 的 __SVC。 示例按三个工具链分别给出 __SVC 的内联实现,执行 SVC 0x01 指令进入 SVCall 异常,异常返回后继续执行。
#if defined ( __CC_ARM )
__ASM void __SVC(void)
{
SVC 0x01
BX R14
}
#elif defined ( __ICCARM__ )
static __INLINE void __SVC() { __ASM ("svc 0x01");}
#elif defined ( __GNUC__ )
static __INLINE void __SVC() { __ASM volatile ("svc 0x01");}
#endif
4. 在 SVC_Handler 里把 CONTROL 清零切回特权。 异常模式下可以改写 CONTROL,处理器把线程模式重新置回特权访问后返回。
void SVC_Handler(void)
{
/* Switch Thread mode from privileged to unprivileged */
__set_CONTROL(0x00);
}
5. 回切后再次检查特权位。 从异常返回后再读 CONTROL,确认线程模式已回到特权访问并打印结果。
/* Check Thread mode privilege status */
if ((__get_CONTROL() & 0x01) == THREAD_MODE_PRIVILEGED)
{
/* Thread mode has privileged access */
ThreadMode = THREAD_MODE_PRIVILEGED;
printf("Cortex-M4F enter Privileged Mode OK \r\n");
}
else
{
/* Thread mode has unprivileged access */
ThreadMode = THREAD_MODE_UNPRIVILEGED;
printf("Cortex-M4F enter Privileged Mode Fail \r\n");
}
对应源码: src/main.c、src/n32h49x_it.c
5.4.4 注意与排障
| 场景 | 检查与建议 | 来源 |
|---|---|---|
| 位段区的覆盖范围 | 只有 SRAM、CCM 与外设地址空间映射到位段区,可经别名区原子访问; 其余地址空间没有位段别名。 |
用户手册 |
| 别名地址的计算公式 | 别名地址按 bitband_byte_addr = bitband_base + (byte_offset x 32) + (bit_number x 4) 计算,bit_number 取值为 0 到 7。 |
用户手册 |
| 位段写操作是原子的 | 对别名区写一个字等价于对位段区目标位做读-改-写,因此位段写是原子的; 读别名地址返回该位为 0x01 或 0x00。 |
用户手册 |
| SRAM 别名地址示例 | 例如 SRAM 地址 0x20000400 的位 4 映射到别名地址 0x22008010; 写该别名地址等同于对 0x20000400 的位 4 做读-改-写。 |
用户手册 |