C语言在嵌入式中的特殊功能及编译优化

C语言在嵌入式中的特殊功能

注意:指针的使用及内存操作是嵌入式C语言的重点。

1.寄存器/端口和位带操作

在嵌入式系统编程中,C语言被广泛使用来与硬件进行交互。其中,嵌入式寄存器、端口和位带操作是非常常见的操作。

  1. 嵌入式寄存器:
    嵌入式寄存器是存储在硬件上的特殊内存位置,用于控制和配置硬件的行为。在C语言中,我们通常使用结构体或联合体来定义嵌入式寄存器。

例如:

struct registers {
    volatile uint8_t register1;
    volatile uint8_t register2;
    // 其他寄存器
};

这里的volatile关键字告诉编译器不要对这个结构体的成员进行优化,因为它们可能会被硬件直接干预。
2. 嵌入式端口:
嵌入式端口是硬件上的一组引脚,通常用于输入和输出数据。在C语言中,我们可以使用特定的库函数或者直接访问端口地址来进行操作。

例如,使用Keil C51库函数:

#include <reg51.h> // 包含Keil C51的特定头文件

// 设置P0端口的第0位为高电平
P0 = 0x01;

// 声明一个名为 bit LED0 的变量,该变量的类型为 bit。  
// bit 是 C51 系列单片机特有的数据类型,用于表示单片机的某个位。  
bit LED0;  
  
// 将 P1^0 赋值给 LED0 变量。  
// P1^0 表示单片机的 P1 端口的第 0 位。  
// 在 C51 系列单片机中,可以通过直接操作端口的某一位来控制对应的硬件设备。  
LED0 = P1^0;

或者直接访问端口地址(这种方法不依赖于特定的编译器或硬件):

#define PORT_ADDRESS 0x0003 // 假设P0端口地址为0x0003
*(unsigned char*)PORT_ADDRESS = 0x01; // 设置P0端口的第0位为高电平
  1. 位带操作:
    位带操作是一种对给定对象的某一位或一组位进行操作的技术。在C语言中,可以使用位运算符来执行位带操作。

例如:

unsigned char value = 0x42; // 二进制表示为0100 0010
unsigned char bit0 = (value & 0x01) != 0; // 检查value的第0位是否为1,如果不是,结果为0,否则为1
unsigned char bit1 = (value & 0x02) != 0; // 检查value的第1位是否为1,如果不是,结果为0,否则为1

在这个例子中,我们使用&运算符和位掩码来获取给定值的某一位。位掩码是一个只有一位为1,其他位为0的值。通过将位掩码与给定值进行与操作,可以获取给定值中的指定位。

2.中断服务函数和定时器

stm32

在嵌入式系统中,中断服务函数(Interrupt Service Routine,ISR)是一个非常重要的概念。当某些特定事件(如定时器溢出、外部中断触发等)发生时,处理器会暂停当前执行的程序,跳转到一个预定的地址执行中断服务函数。

在C语言中,通常使用函数指针来注册中断服务函数。下面是一个简单的例子,演示如何在ARM Cortex-M4处理器上注册一个中断服务函数:

#include <stdint.h>
#include <stm32f4xx.h>

// 定义中断服务函数
typedef void (*isr_t)(void)// 中断服务函数,进入此函数时会保存当前CPU状态
// 退出时需要进行状态恢复
void timer_isr(void)
{
  // 处理定时器溢出中断
  
  // 清除中断标志位
  TIM3->SR = ~(1<<0); 
}

int main(void)
{
  // 初始化系统时钟等

  // 初始化定时器3
  RCC->APB1ENR |= (1<<1); // 使能定时器3时钟
  
  // 配置定时器溢出中断
  TIM3->DIER |= (1<<0); // 使能更新中断
  TIM3->PSC = 8399; // 预分频,定时器计数速率为1MHz
  TIM3->ARR = 5000; // 自动重装载值,计数到5000产生中断  
  TIM3->CNT = 0; // 清空计数器  
  TIM3->CR1 |= (1<<0); // 使能定时器3
  
  // 注册中断服务函数
  isr_t isr; 
  isr = &timer_isr;
  NVIC_SetPriority(TIM3_IRQn, 0); // 设置优先级 
  NVIC_EnableIRQ(TIM3_IRQn); // 使能中断
  
  while(1){
    // 程序主循环
  }
}

运行过程:
1. 主程序初始化定时器3,配置预分频、自动重装值,清空计数器,启动定时器。
2. 主程序注册timer_isr()为中断服务函数,设置优先级,并使能中断。
3. 定时器开始运行,当计数值达到自动重装值5000,产生溢出中断。
4. CPU跳转到timer_isr()中断服务函数执行。
5. timer_isr()清除中断标志位,处理中断。
6. timer_isr()执行完毕后,CPU返回到主程序继续执行。

这是一个非常基本的例子,它展示了如何在C语言中注册一个中断服务函数。这个例子中用到的函数包括:

  • TIM3->PSC = 8399;:设置定时器的预分频值。预分频值决定了定时器溢出速度,即定时器计数值达到最大值的速度。这个值可以根据需要进行调整。
  • TIM3->ARR = 1000;:设置定时器的自动重装载值。这个值决定了定时器计数的最大值。当定时器计数达到这个值时,定时器会溢出,并触发中断。
  • TIM3->CNT = 0;:清零定时器计数值。在开始一个定时器计时周期之前,通常需要将定时器计数值清零。
  • TIM3->CR1 = 1;:启动定时器。当这个值为1时,定时器开始计数。
  • NVIC_SetPriority(TIM3_IRQn, 0);:设置中断优先级。这个函数的第一个参数是中断源,第二个参数是优先级。优先级越低,表示优先级越高。在这个例子中,我们将优先级设置为0,表示最高优先级。
  • NVIC_EnableIRQ(TIM3_IRQn);:启用中断。当这个函数被调用时,相应的中断源被启用,当满足触发条件时,处理器会跳转到对应的中断服务函数执行。

在嵌入式系统中,中断服务函数(ISR)经常与定时器搭配使用。

  • 定时器可以生成定时中断,当发生定时中断时,就会触发中断服务函数的执行。
  • 在中断服务函数中,我们可以做一些需要定期执行的任务,比如:
    • 刷新LED
    • 读取传感器数据
    • 计算脉冲计数
    • 发送串口数据等
  • 定时器+中断服务函数的组合,可以确保某些任务定期、自动地执行,不需要我们在主程序中反复判断。
  • 常见的定时器包括系统定时器、通用定时器、APIC定时器等。
  • 在中断服务函数中需要快速完成任务并退出,不要有大量处理或阻塞,否则会影响整个系统的实时性。
    总之,中断服务函数和定时器在嵌入式系统中通常是配合使用的,这对于实时应用和处理定时任务非常重要。正确使用定时器中断和中断服务函数是嵌入式C编程的重要内容之一。
完整任务中断
	后面单独一讲吧,涉及方面太多

51

  1. 51单片机有多个定时/计数器,如T0、T1等。它们溢出时可触发中断,调用中断服务函数。
  2. 51单片机的定时/计数器溢出中断通过设置TCON控制寄存器的中断使能位来开启。
  3. 当定时/计数器溢出时,会设置中断标志位。中断系统检测到标志位后,会调用与该定时器相应的中断服务函数。
  4. 中断服务函数需要保存现场,在最后使用RET或RETI指令返回。51提供了关中断和开中断指令方便此过程。
  5. 在ISR内,可以重新加载定时初值,从而实现周期性溢出中断,完成定时功能。
  6. 通过读取定时/计数器的值,可以计算时间差或测量信号脉冲宽度。
  7. 可以在ISR内启动/停止定时/计数器,实现更灵活的控制。
  8. 通过设置中断优先级,可以控制高优先级中断打断低优先级ISR的执行。
  9. 对关键任务,可以检测ISR是否在特定时间内执行,实现监视机制。
  10. 多个定时器可以共享一个ISR,也可以使用多个ISR。
// 定时器初始化
void TIM2_Init(void)
{
  // 使能定时器时钟,对TIM2定时器的时钟进行使能,
  // 这样才可以对TIM2定时器进行配置
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); 

  // 定时器配置
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  // 设置自动重载周期值为9999,即计数到9999后重新从0开始计数
  TIM_TimeBaseStructure.TIM_Period = 9999;
  // 设置预分频系数为7199,即时钟频率为72MHz/7199 = 10KHz  
  TIM_TimeBaseStructure.TIM_Prescaler = 7199; 
  // 设置时钟分频因子,配置死区时间时需要用到   
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
  // 向上计数模式
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;   
  // 初始化定时器
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

  // 使能定时器
  TIM_Cmd(TIM2, ENABLE); 
}

// 中断初始化
void NVIC_Config(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
  
  // 设置优先级分组,4bit 抢占优先级,0bit 响应优先级
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

  // 配置TIM2定时器中断   
  NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
  // 设置抢占优先级为1
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 
  // 设置响应优先级为1
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  // 使能中断
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  // 初始化配置NVIC
  NVIC_Init(&NVIC_InitStructure);
}

// 中断服务函数 
void TIM2_IRQHandler(void)
{
  // 判断定时器更新中断是否发生
  if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
  {   
    // 清除中断标志位
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    
    // 处理10ms定时任务
    Handle_10ms_Tasks();
  }
}

int main(void)
{
  // 初始化
  TIM2_Init();
  NVIC_Config();
  
  while(1)
  {
    // 主程序执行其他任务
  }
}

首先是TIM2_Init()函数,使能TIM2定时器时钟,然后配置定时器自动重载周期和预分频值,以生成10KHz的定时中断。
NVIC_Config()函数配置TIM2定时器中断,设置优先级并使能中断。
在TIM2_IRQHandler()中断服务函数中,判断是否发生了定时器更新中断,如果是则清除中断标志位,并调用Handle_10ms_Tasks()函数处理10ms周期的任务。
main()函数中先初始化定时器和中断,然后在循环中执行其他主程序任务。
这样通过定时器中断和中断服务函数,可以定期执行10ms周期的任务,而主程序不会被中断占用,可以继续执行其他任务,实现了定时任务与主程序任务的并行执行。

完整任务中断
	后面单独一讲吧,涉及方面太多

3.I/O端口读写

32

端口模式,数字输入输出、模拟输入、推挽输出、开漏输出、复用输出和硬件中断
功能/模式描述如何设置模式用途
数字输入输出GPIO端口可以作为数字输入或输出使用。当作为输入使用时,可以从外部设备读取数字信号;当作为输出使用时,可以向外部设备发送数字信号。配置GPIO端口为输入或输出模式,使用HAL库函数(如HAL_GPIO_Init())进行初始化。读取外部设备的数字信号,控制外部设备的数字信号。
模拟输入一些STM32型号的GPIO端口可以作为模拟输入使用,可以读取外部设备的模拟信号。配置GPIO端口为模拟输入模式,使用HAL库函数(如HAL_GPIO_Init())进行初始化。读取外部设备的模拟信号,如传感器输出的电压值。
推挽输出GPIO端口可以配置为推挽输出模式,在这种模式下,GPIO引脚可以作为高电平或低电平输出。配置GPIO端口为推挽输出模式,使用HAL库函数(如HAL_GPIO_Init())进行初始化。控制外部设备的数字信号,如LED灯的亮灭状态。
开漏输出GPIO端口可以配置为开漏输出模式,在这种模式下,GPIO引脚只能输出低电平信号。当输出高电平时,输出为高阻态,需要外部上拉电阻将引脚拉低。配置GPIO端口为开漏输出模式,使用HAL库函数(如HAL_GPIO_Init())进行初始化。控制外部设备的数字信号,如LED灯的亮灭状态,适用于需要外部上拉电阻的电路。
复用输出GPIO端口可以配置为复用输出模式,在这种模式下,GPIO引脚可以输出内部信号(如TIMx_CHx),也可以输出外部信号。配置GPIO端口为复用输出模式,使用HAL库函数(如HAL_GPIO_Init())进行初始化。选择要输出的内部或外部信号,通过相应的读写操作进行控制。控制内部定时器信号或外部设备的数字信号,如控制TIMx通道的输出信号。
硬件中断GPIO端口可以配置为硬件中断源,当GPIO引脚状态发生变化时,可以触发中断。配置GPIO端口为中断源模式,使用HAL库函数(如HAL_GPIO_Init())进行初始化。根据需要配置中断触发方式(上升沿、下降沿等)。在中断处理程序中响应GPIO引脚状态变化的中断事件,执行相应的操作。

在使用过程中,根据具体的应用场景选择合适的配置和使用方式。通过初始化GPIO端口为相应的模式,并使用相应的读写操作进行控制和操作。同时,注意根据硬件电路的特点选择合适的输出类型和配置参数。

好的,以下是设置GPIO端口模式的操作:

  1. 数字输入输出模式:
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 设置要初始化的GPIO引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN; // 设置GPIO模式为输入模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA端口,根据GPIO_InitStruct进行初始化配置
  1. 模拟输入模式:
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 设置要初始化的GPIO引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; // 设置GPIO模式为模拟输入模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA端口,根据GPIO_InitStruct进行初始化配置
  1. 推挽输出模式:
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 设置要初始化的GPIO引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT; // 设置GPIO模式为输出模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA端口,根据GPIO_InitStruct进行初始化配置
  1. 开漏输出模式:
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 设置要初始化的GPIO引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OPENDRAIN; // 设置GPIO模式为开漏输出模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA端口,根据GPIO_InitStruct进行初始化配置

案例代码:

#include "stm32f4xx.h"

int main(void)
{
  GPIO_InitTypeDef GPIO_InitStruct;
  uint8_t ledStatus = 0x00;

  // 初始化GPIO端口,用于控制LED灯
  GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 选择LED连接的引脚
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz输出速度
  GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; // 推挽输出类型
  GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL; // 不需要上下拉
  GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA端口

  while (1)
  {
    // 关闭所有LED灯
    GPIO_WriteBit(GPIOA, GPIO_Pin_13, Bit_RESET);
    delay(500); // 延时一段时间

    // 打开所有LED灯
    GPIO_WriteBit(GPIOA, GPIO_Pin_13, Bit_SET);
    delay(500); // 延时一段时间
  }
}

上述代码实现了使用STM32F4系列单片机的GPIO端口控制LED灯的闪烁。首先在main()函数中初始化了用于控制LED灯的GPIO端口,然后在一个无限循环中交替关闭和打开LED灯,并延时一段时间以实现闪烁效果。其中GPIO_InitTypeDef结构体用于配置GPIO端口,GPIO_Pin成员表示引脚号,GPIO_Mode成员表示工作模式,GPIO_Speed成员表示输出速度,GPIO_OType成员表示输出类型,GPIO_PuPd成员表示上下拉电阻。GPIO_WriteBit()函数用于写入指定引脚的状态。

51

#include <reg51.h>  
  
#define LED P1 // 将LED连接到端口P1  
bit LED1 = P0^0  //将LED1连接到端口P0的第一个端口,计算机从0开始 
  
void delay(unsigned int time) // 延时函数  
{  
    unsigned int i, j;  
    for(i=0; i<time; i++)  
        for(j=0; j<1275; j++);  
}  
  
void main()  
{  
    while(1)  
    {  
    //注意具体开LED灯的电路,0和1都有可能亮
    	LED1 = 1;//关闭LED1
        LED = 0x00; // 关闭所有LED灯  
        delay(500); // 延时一段时间  
        LED = 0xFF; // 打开所有LED灯  
        delay(500); // 延时一段时间  
        LED1 = 0;//关闭LED1
        
    }  
}

4.关于编译优化的概念

第一算法优化,

  • 各种算法
    第二gcc 自带的优化

1. 内联优化

  • inline 属性:给函数声明加上inline属性,建议编译器进行内联
  • attribute((always_inline)) :强制编译器内联该函数
  • attribute((noinline)):告诉编译器不内联该函数
    未优化:
void sum(int a, int b) {
  return a + b;
}

int main() {
  int s = 0;
  for (int i = 0; i < 100; i++) {
    s += sum(i, i+1); 
  }
  return s;
}

优化:

// 建议内联 
inline void sum(int a, int b) {
  return a + b; 
}

// 或者强制内联
void sum(int a, int b) __attribute__((always_inline)) {
  return a + b;
}

int main() {
  // 代码不变
}

内联可以减少函数调用开销,但会增加代码大小。

2. 分支优化

  • __builtin_expect:给分支预测可能性,优化分支预取
  • __builtin_unreachable:说明该代码路径不可能达到,可以优化掉
  • 未优化:
int foo(int a) {
  if (a > 0) {
    return 1;
  } else {
    return -1;
  }
}

优化:

int foo(int a) {
  if (__builtin_expect(a > 0, 1)) {
    return 1;
  } else {
     return -1; 
  }
}

这里使用__builtin_expect提示a>0的分支更可能发生,编译器可以进行预测性优化。
另一个例子:

void foo() {
  // 一些代码

  __builtin_unreachable();

  // 不可达代码
}

使用__builtin_unreachable可以让编译器知道该代码不可达,优化掉后面的代码。

3. 循环优化

  • #pragma GCC unroll N:循环展开N次
  • #pragma GCC ivdep:消除循环流依赖,进行并行优化
  1. 循环展开
    未优化:
void func(int n) {
  for (int i = 0; i < n; i++) {
    // 循环体 
  }
}

优化:

void func(int n) {
  #pragma GCC unroll 16 
  for (int i = 0; i < n; i++) {
    // 循环体
  } 
}

这里使用unroll展开循环,可以减少循环迭代的开销。
2. 消除数据依赖
未优化:

void func(float a[], float b[], float c[]) {
  for (int i = 0; i < n; i++) {
    c[i] = a[i] + b[i]; 
  }
}

优化:

void func(float a[], float b[], float c[]) {
  #pragma GCC ivdep
  for (int i = 0; i < n; i++) {
    c[i] = a[i] + b[i];
  }
}
使用ivdep可以提示编译器这个循环可以并行计算。

4. 数据对齐

  • attribute((aligned(N))):数据对齐到N字节边界
  • attribute((packed)):取消数据在结构体中的对齐
  1. 数据对齐
    未优化:
struct Data {
  char a; 
  int b;
  float c;
}

优化:

struct Data {
  char a;
  int b __attribute__((aligned(8))); 
  float c; 
}

这里对int类型进行8字节对齐,可以优化访问效率。
2. 取消结构体对齐
未优化:

struct Data {
  char a;
  int b;
  double c; 
}

优化:

struct __attribute__((packed)) Data {
  char a;
  int b;
  double c;
} 

使用packed取消结构体对齐,可以减小结构体大小。
3. 对照案例
未优化:

void access(Struct data) {
  printf("%d", data.b); 
}

优化:

void access(Struct __attribute__((aligned(16))) data) {
  printf("%d", data.b);
}

对比可以看出,aligned可以提高数据访问效率。

5. 优化层级控制

  • #pragma GCC optimize(“O0”):关闭优化
  • #pragma GCC optimize(“O1”):默认优化
  • #pragma GCC optimize(“O2”):全量优化
  • #pragma GCC optimize(“O3”):进一步优化
  • #pragma GCC optimize(“Os”):优化代码大小
  1. 关闭优化
#pragma GCC optimize("O0") 
void func() {
  // 代码
}

使用O0关闭优化,用于调试。
2. 默认优化

#pragma GCC optimize("O1")
void func() {
  // 代码
}

O1进行基本优化。
3. 全量优化

#pragma GCC optimize("O2") 
void func() {
  // 代码 
}

O2进一步优化。
4. 进一步优化

#pragma GCC optimize("O3")
void func() {
  // 代码
}

O3进行编译器支持的最佳优化。
5. 优化代码大小

#pragma GCC optimize("Os")
void func() {
  // 代码
}

Os优化目标是最小的代码大小。

关于嵌入式任务系统内容太多,以后文章再写

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值