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 开始,两个基址都先定义成宏。

src/main.csrc/main.c
/* 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。

src/main.csrc/main.c
    /* 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. 读一位用右移取位。 读操作不走别名区,直接把变量右移到位号处取最低位即可。

src/main.csrc/main.c
#define Data_GetBit_BB(DataAddr, BitNumber) (Data >> BitNumber)

4. 循环用别名区原子置位与清位。 每轮先把第 i 位置 1 并打印整字,再把第 i 位清 0 并打印整字,逐位验证别名写只影响目标位。

src/main.csrc/main.c · main
    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 由编译器按目标设置给出,示例用它选择打印的标签,硬件浮点与软件浮点用同一份代码。

src/main.csrc/main.c
#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,周期取最大值,测量前清零并启动计数。

src/main.csrc/main.c · InitTIMER
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 时这些运算改由软件浮点库完成,因而明显更慢。

src/julia_fpu.csrc/julia_fpu.c · GenerateJulia_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 就得到毫秒数,标签与耗时一起打印。

src/main.csrc/main.c · main
        /* 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 选择进程栈。

src/main.csrc/main.c · main
    /* 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 进入非特权访问,再读回该位判断是否生效。

src/main.csrc/main.c · main
    /* 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 异常,异常返回后继续执行。

src/main.csrc/main.c
#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,处理器把线程模式重新置回特权访问后返回。

src/n32h49x_it.csrc/n32h49x_it.c · SVC_Handler
void SVC_Handler(void)
{
    /* Switch Thread mode from privileged to unprivileged */
    __set_CONTROL(0x00);
}

5. 回切后再次检查特权位。 从异常返回后再读 CONTROL,确认线程模式已回到特权访问并打印结果。

src/main.csrc/main.c · main
    /* 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 做读-改-写。
用户手册
国民技术开发套件文档中心

按芯片系列在线查阅国民技术 MCU 开发套件的芯片资料、SDK、驱动 API 与 Demo 示例说明。

© 2026 国民技术股份有限公司 版权所有粤ICP备11069485号本站使用不含 Cookie 的匿名访问统计