Arm C语言编程实例有哪些实用技巧?

99ANYc3cd6
预计阅读时长 27 分钟
位置: 首页 C语言 正文

这些实例将围绕 ARM Cortex-M 系列(如 STM32, NXP LPC, TI Tiva 等)展开,因为这是目前最主流的 32 位 ARM 微控制器架构,实例会使用 Keil MDK 作为开发环境,并基于 STM32F103C8T6 这款经典的“蓝丸”开发板进行讲解。

arm c语言编程实例
(图片来源网络,侵删)

实例 1:最基础的点灯程序 - GPIO 输出

这是嵌入式世界的 "Hello, World!",目标是控制一个 LED 灯的亮灭。

硬件准备

  • 开发板: STM32F103C8T6 (通常板载一个 LED 连接到 PC13 引脚)
  • 工具: Keil MDK (或其他 IDE 如 STM32CubeIDE)

理解 STM32 的 GPIO

与简单的 51 单片机不同,STM32 的 GPIO 功能非常强大,需要通过配置多个寄存器来设置其工作模式、速度、上下拉等。

对于 STM32F103,我们需要操作以下几个关键的寄存器来配置 PC13 引脚:

  • RCC_APB2ENR (时钟使能寄存器): 必须先给 GPIOC 端口使能时钟,否则无法操作其寄存器。
  • GPIOC_CRH (端口配置高寄存器): PC13 是一个高引脚号,所以配置在 CRH 寄存器中,我们需要将其设置为推挽输出模式。
  • GPIOC_ODR (端口输出数据寄存器): 写 1 使引脚输出高电平(熄灭 LED),写 0 使引脚输出低电平(点亮 LED,假设是低电平点亮)。
  • GPIOC_BSRR (端口位设置/复位寄存器): 这是一个更方便的寄存器,可以原子地设置或复位某个引脚,避免在多线程中出现问题。

C 语言代码实现

#include "stm32f10x.h" // 包含 STM32F10x 的头文件
// 简单延时函数
void simple_delay(volatile uint32_t count) {
    while(count--);
}
int main(void) {
    // 1. 使能 GPIOC 的时钟
    // RCC_APB2ENR 寄存器的第 4 位 (IOPCEN) 控制 GPIOC 的时钟
    RCC->APB2ENR |= (1 << 4); // 使用位带操作或直接赋值,这里用按位或确保不修改其他位
    // 2. 配置 PC13 为推挽输出模式
    // GPIOC_CRH 的第 20-21 位 (CNF13) 设置为 00 (推挽)
    // GPIOC_CRH 的第 16-19 位 (MODE13) 设置为 01 (输出模式,最大速度 10MHz)
    // 原始值: 0x44444444
    // 对 PC13 的配置: 位 [21:20] = 00, 位 [19:16] = 0001
    // CRH 寄存器需要设置为: 0x44444444 | (0x0 << 20) | (0x1 << 16)
    // 简化操作:先清零,再设置
    GPIOC->CRH &= ~(0x0F << 16); // 清除 PC13 的原有配置 (4位)
    GPIOC->CRH |= (0x01 << 16);  // 设置为推挽输出,最大速度 10MHz
    // 3. 主循环
    while (1) {
        // 点亮 LED (PC13 输出低电平)
        // 方法一:直接操作 ODR 寄存器
        // GPIOC->ODR &= ~(1 << 13); // 将第 13 位置 0
        // 方法二:使用 BSRR 寄存器(推荐,更安全)
        GPIOC->BSRR = (1 << 13); // BSRR 的低 16 位用于复位,写 1 复位对应引脚
        simple_delay(500000); // 延时
        // 熄灭 LED (PC13 输出高电平)
        // GPIOC->ODR |= (1 << 13); // 将第 13 位置 1
        // 使用 BSRR 寄存器
        GPIOC->BSRR = (1 << (13 + 16)); // BSRR 的高 16 位用于置位,写 1 置位对应引脚
        simple_delay(500000); // 延时
    }
}

代码解析

  1. #include "stm32f10x.h": 包含了 STM32F10x 所有寄存器定义的内存映射文件,这是直接操作寄存器的基础。
  2. RCC->APB2ENR |= (1 << 4);: 这是 至关重要 的一步,ARM Cortex-M 采用的是总线架构,外设(如 GPIO)必须先被“时钟使能”,才能被 CPU 访问和控制。-> 结构体指针访问方式是操作 SFR(特殊功能寄存器)的标准方法。
  3. GPIOC->CRH &= ...; GPIOC->CRH |= ...;: 这里对 CRH 寄存器进行了“读-改-写”操作,精确地配置了 PC13 的引脚功能。
  4. GPIOC->BSRR = ...;: 这是 Cortex-M GPIO 的一个特色寄存器,它允许你原子地(在单个总线周期内)设置或复位引脚,避免了在复杂系统中因竞争条件导致的问题,低 16 位用于复位(输出低),高 16 位用于置位(输出高)。

实例 2:按键检测 - GPIO 输入

本实例将读取一个按键的状态,当按键按下时,点亮 LED。

arm c语言编程实例
(图片来源网络,侵删)

硬件准备

  • 开发板: STM32F103C8T6
  • 按键: 一个按键,一端接 GND,另一端接一个 GPIO 引脚(PA0)。

理解 GPIO 输入

为了读取按键输入,我们需要:

  • 时钟使能: 同样需要使能 GPIOA 的时钟。
  • 模式配置: 将 PA0 配置为输入模式。
    • 浮空输入: 按键不接上下拉电阻,状态不确定。
    • 上拉输入: 默认高电平,按键按下后变为低电平。
    • 下拉输入: 默认低电平,按键按下后变为高电平。
    • 模拟输入: 用于 ADC。
  • 读取状态: 通过读取 GPIOA->IDR (端口输入数据寄存器) 来获取引脚状态。

C 语言代码实现

#include "stm32f10x.h"
void simple_delay(volatile uint32_t count) {
    while(count--);
}
int main(void) {
    // 1. 使能 GPIOA 和 GPIOC 的时钟
    RCC->APB2ENR |= (1 << 2); // 使能 GPIOA
    RCC->APB2ENR |= (1 << 4); // 使能 GPIOC
    // 2. 配置 PA0 为上拉输入
    // GPIOA_CRL 的位 [1:0] (MODE0) = 00 (输入)
    // GPIOA_CRL 的位 [3:2] (CNF0) = 10 (上拉/下拉)
    // 同时设置 ODR0 位为 1,选择上拉
    GPIOA->CRL &= ~(0x0F << 0); // 清除 PA0 的原有配置
    GPIOA->CRL |= (0x08 << 0);  // 设置为上拉/下拉输入模式
    GPIOA->ODR |= (1 << 0);     // 选择上拉
    // 3. 配置 PC13 为推挽输出 (同实例1)
    GPIOC->CRH &= ~(0x0F << 16);
    GPIOC->CRH |= (0x01 << 16);
    // 4. 主循环
    while (1) {
        // 读取 PA0 的状态
        // IDR 寄存器,位 0 对应 PA0
        if ((GPIOA->IDR & (1 << 0)) == 0) {
            // 如果为 0,说明按键按下(低电平)
            // 点亮 LED
            GPIOC->BSRR = (1 << 13); // 复位 PC13
        } else {
            // 否则,按键未按下(高电平)
            // 熄灭 LED
            GPIOC->BSRR = (1 << (13 + 16)); // 置位 PC13
        }
        // 简单的消抖延时
        simple_delay(10000);
    }
}

代码解析

  1. GPIOA->CRL |= (0x08 << 0);: 将 PA0 配置为上拉/下拉输入模式。
  2. GPIOA->ODR |= (1 << 0);: 在配置上拉/下拉模式后,必须通过 ODR 寄存器来选择是上拉还是下拉。ODR 对应位写 1 为上拉,写 0 为下拉。
  3. if ((GPIOA->IDR & (1 << 0)) == 0): 通过按位与操作 & 来检测 IDR 的第 0 位是否为 0。== 0 是为了确保整个表达式的结果是 0(假)或非 0(真)。

实例 3:使用标准外设库 - 控制串口

直接操作寄存器虽然高效,但代码复杂且不易移植。STM32 标准外设库 提供了封装好的函数,让代码更清晰、更易于维护。

本实例将通过串口发送 "Hello, ARM!\r\n" 字符串。

硬件准备

  • 开发板: STM32F103C8T6
  • USB 转串口模块: 连接 PA9 (TX) 和 PA10 (RX) 到电脑。

库文件准备

在 Keil MDK 工程中,你需要添加以下库文件和文件夹:

arm c语言编程实例
(图片来源网络,侵删)
  • STM32F10x_StdPeriph_Driver 文件夹
  • CMSIS 文件夹
  • 并在 Options for Target -> C/C++ -> Include Paths 中添加这些路径。

C 语言代码实现

#include "stm32f10x.h"
#include "stdio.h" // 为了使用 printf
// 重定向 printf 到串口
int fputc(int ch, FILE *f) {
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送寄存器为空
    USART_SendData(USART1, (uint8_t)ch);
    return ch;
}
void USART1_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
    // 2. 配置 USART1 TX (PA9) 和 RX (PA10) 引脚
    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);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    // 3. USART1 参数配置
    USART_InitStructure.USART_BaudRate = 115200;    // 波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;      // 1位停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;         // 无校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(USART1, &USART_InitStructure);
    // 4. 使能 USART1
    USART_Cmd(USART1, ENABLE);
}
int main(void) {
    USART1_Init();
    printf("Hello, ARM!\r\n");
    printf("This is a message from STM32 using Standard Peripheral Library.\r\n");
    while (1) {
        // 主循环可以执行其他任务
    }
}

代码解析

  1. #include "stm32f10x.h": 包含了所有库函数的声明。
  2. GPIO_InitTypeDefUSART_InitTypeDef: 这是 SPL 的核心,使用结构体来封装寄存器配置,你只需要填充结构体成员,然后调用初始化函数,库会自动完成寄存器的设置。
  3. RCC_APB2PeriphClockCmd(...): 一个函数调用就完成了多个外设的时钟使能,比手动操作寄存器方便得多。
  4. USART_Init(): 填充 USART_InitTypeDef 结构体后,调用此函数完成串口的所有底层配置。
  5. printf 重定向: 通过重写 fputc 函数,我们可以使用标准 C 库的 printf 函数,让串口打印调试信息变得非常简单。USART_GetFlagStatusUSART_SendData 都是库提供的函数。

总结与进阶

实例 核心概念 关键技术点 适用场景
点灯 GPIO 输出 寄存器操作、时钟使能、推挽输出 理解底层硬件,最简单的控制
按键 GPIO 输入 寄存器操作、上拉/下拉输入、消抖 读取外部信号,人机交互基础
串口 通信、SPL 标准外设库、printf重定向、中断(可扩展) 调试、数据传输、与PC通信

进阶方向:

  1. 中断编程: 学习使用 NVIC (嵌套向量中断控制器) 来处理按键中断、串口接收中断等,提高系统的实时性和效率。
  2. 定时器: 学习使用 SysTick (系统滴答定时器) 或通用定时器 来实现精确的延时、PWM 输出(控制舵机、LED 亮度)、输入捕获(测量频率)等。
  3. DMA (直接内存访问): 学习使用 DMA 来搬运数据,例如让 DMA 将串口接收到的数据直接存入内存,解放 CPU。
  4. RTOS (实时操作系统): 学习使用 FreeRTOS、RT-Thread 等轻量级操作系统,管理多个任务,实现更复杂的并发逻辑。

这些实例为你打下了坚实的基础,从直接操作寄存器理解硬件本质,到使用标准库提高开发效率,是 ARM C 语言学习的必经之路,希望这些实例能帮助你更好地入门!

-- 展开阅读全文 --
头像
C语言如何直接调用Java方法?
« 上一篇 04-29
C语言if not函数如何实现?
下一篇 » 04-29

相关文章

取消
微信二维码
支付宝二维码

目录[+]