C语言在嵌入式中的特殊功能
注意:指针的使用及内存操作是嵌入式C语言的重点。
1.寄存器/端口和位带操作
在嵌入式系统编程中,C语言被广泛使用来与硬件进行交互。其中,嵌入式寄存器、端口和位带操作是非常常见的操作。
- 嵌入式寄存器:
嵌入式寄存器是存储在硬件上的特殊内存位置,用于控制和配置硬件的行为。在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位为高电平
- 位带操作:
位带操作是一种对给定对象的某一位或一组位进行操作的技术。在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
- 51单片机有多个定时/计数器,如T0、T1等。它们溢出时可触发中断,调用中断服务函数。
- 51单片机的定时/计数器溢出中断通过设置TCON控制寄存器的中断使能位来开启。
- 当定时/计数器溢出时,会设置中断标志位。中断系统检测到标志位后,会调用与该定时器相应的中断服务函数。
- 中断服务函数需要保存现场,在最后使用RET或RETI指令返回。51提供了关中断和开中断指令方便此过程。
- 在ISR内,可以重新加载定时初值,从而实现周期性溢出中断,完成定时功能。
- 通过读取定时/计数器的值,可以计算时间差或测量信号脉冲宽度。
- 可以在ISR内启动/停止定时/计数器,实现更灵活的控制。
- 通过设置中断优先级,可以控制高优先级中断打断低优先级ISR的执行。
- 对关键任务,可以检测ISR是否在特定时间内执行,实现监视机制。
- 多个定时器可以共享一个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端口模式的操作:
- 数字输入输出模式:
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进行初始化配置
- 模拟输入模式:
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进行初始化配置
- 推挽输出模式:
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进行初始化配置
- 开漏输出模式:
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:消除循环流依赖,进行并行优化
- 循环展开
未优化:
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)):取消数据在结构体中的对齐
- 数据对齐
未优化:
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”):优化代码大小
- 关闭优化
#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优化目标是最小的代码大小。