由于笔者工作是属于嵌入式方面,所以文章的C的编程思想跟算法,数据结构无很大关系,主要是功能应用的使用。
常用的编程思想
模块化编程思想:
回主目录
模块化编程思想将整个系统功能代码分割成小的、互不依赖的功能,通过定义函数和模块化的方式进行各个功能实现。这样做有助于复用和代码维护,可让多个人同时开发一个项目。
/*输入一个数,获取它的平方值*/
/*函数功能:计算平方*/
int square(int num) {
return num * num;
}
/*函数功能:打印计算值*/
void printSquare(int num) {
int result = square(num);
printf("平方值:%d\n", result);
}
// 主函数
int main() {
int number = 5;
printSquare(number);
return 0;
}
面向对象思想:
尽管C语言中没有类和对象的概念,但你可以使用结构体来模拟对象,并通过函数指针实现类似于类的行为和接口。
例如,假设你要实现一个简单的字符串处理库,包括创建字符串、拷贝字符串、连接字符串等操作。你可以使用结构体来表示字符串对象,并定义与之相关的操作函数,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
int length;
} String;
String* create_string(const char *data) {
String* str = malloc(sizeof(String));
str->length = strlen(data);
str->data = malloc((str->length + 1) * sizeof(char));
strcpy(str->data, data);
return str;
}
void destroy_string(String* str) {
free(str->data);
free(str);
}
void print_string(const String* str) {
printf("String: %s\n", str->data);
}
int main() {
String* str = create_string("Hello, World!");
print_string(str);
destroy_string(str);
return 0;
}
状态机思想
可以更好的描述系统的行为,更方便的理解和管理程序。
简介
回主目录
状态机(State Machine)在嵌入式软件编程中是很常见的一种编程思想,它是一种描述和处理系统行为的模型,将系统划分为一组离散的状态,定义了状态之间的转换条件和操作。
状态机由状态(State)
、状态转换条件(Transition)
、动作(Action)
、初始状态(Initial State)
和初始状态(Initial State)
组成。
类型
状态机可以分为两种类型:有限状态机(FSM)和无限状态机(USM)。
- 有限状态机(FSM)是最常见的状态机实现方式。它由确定的状态、预定义的转移条件和对应的动作组成。程序根据当前状态和输入条件,进行状态转移和执行相应的动作。
- 无限状态机(USM)是一种更灵活的状态机实现方式,状态和转移条件可以动态生成和修改。USM通常使用状态模式进行实现,状态之间通过递归调用实现转移,可以处理更复杂的状态转移逻辑。
实现方式:
- 条件逻辑编码法:这种方法通过编写代码来直接实现状态机的转换和动作。一般使用switch语句或、if-else语句或者函数指针等方式来处理不同的事件和状态转换。
- 表驱动法:这种方法利用数据结构来存储状态及其对应的转换条件和动作。通常使用状态转换表(State Transition Table)或状态转换图(State Transition Diagram)来描述状态机的行为。在C语言中,可以使用表格(如二维数组)来表示状态及其转换。通过查表即可根据当前状态和输入事件找到下一个状态,执行相应的动作。
- 状态模式:使用面向对象的方式,将状态抽象为类,每个状态类实现自己的行为和状态迁移逻辑。
基本步骤
-
定义状态:根据具体系统需求,确定状态的数据和含义。每个状态都有一个标识符来表示。
-
定义状态转换条件:确定触发状态转换的条件,即从一个状态转移到另一个状态所需的条件。如输入事件、定时器等。
-
定义动作:确定每个状态下需要执行的动作操作,如更新数据、发送消息等。
-
实现状态机逻辑:将状态、转换条件和动作组织起来,使用代码实现状态机逻辑。可以定义状态机的数据结构和函数,数据结构通常包含当前状态、状态转换表等信息。函数可以用来处理状态转换、执行状态转换动作等。
-
测试和调试:通过测试验证状态机的正确性和稳定性,修复存在的问题。
框架
一、条件逻辑编码
模型一:
/*定义状态A,B,C,每个状态标识一个特定的行为或状态*/
typedef enum{
STATE_A,
STATE_B,
STATE_C,
// ...
} State;
/*定义状态转换条件,开始和停止*/
typedef enum{
EVENT_START,
EVENT_STOP
} Event;
/*实现状态机逻辑*/
void processEvent(Event event,State *currentState)
{
/*根据当前的状态执行对应的动作*/
switch (*currentState) {
case STATE_A:
if (event == EVENT_START)
{
// 状态转移到状态B,执行状态转移时会产生的动作
*currentState = STATE_B;
……
}
break;
case STATE_B:
if (event == EVENT_STOP)
{
//状态转移到状态C,执行状态转移时会产生的动作
*currentState = STATE_C;
……
}
break;
case STATE_C:
// 停止
break;
default:
// 错误处理
printf("错误的状态\n"); /*根据语法规范,错误时最好有个日志输出,方便Debug*/
break;
}
}
int main()
{
State currentState = STATE_A;
processEvent(EVENT_START, ¤tState); //此时跳转到了状态B
processEvent(EVENT_STOP, ¤tState); //此时跳转到了状态C
return 0;
}
模型二:
/*定义状态A,B,C,每个状态标识一个特定的行为或状态*/
typedef enum {
STATE_A,
STATE_B,
STATE_C,
// ...
} State;
/*实现状态机逻辑*/
int main()
{
while(1)
{
switch (current_state)
{
case STATE_A:
// 处理状态A的逻辑
// ...
if(/* 满足某个条件 */)
{
/*触发状态转换条件,状态跳转*/
current_state = STATE_B;
}
else if(/* 满足某个条件 */)
{
current_state = STATE_C;
}
break;
case STATE_B:
// 处理状态B的逻辑
// ...
current_state = STATE_C;
break;
case STATE_C:
// 处理状态C的逻辑
// ...
current_state = STATE_A;
break;
// ...
}
}
}
二、表驱动法
回主目录
表驱动法通过使用一张状态转换表来实现状态机。这张表保存了状态转换的信息,程序会根据当前状态和输入来查找对应的状态转换,并执行相应的操作。
#include <stdio.h>
/*定义状态A,B,C,每个状态标识一个特定的行为或状态*/
typedef enum {
State_A,
State_B,
State_C
} State;
// 定义输入事件
typedef enum {
Event_1,
Event_2
} Event;
// 定义状态转换表
typedef struct {
/*当前状态-当前事件-下个状态-动作*/
State current_state;
Event current_event;
State next_state;
void (*action)();
} Transition;
// 定义状态转换表数组
Transition transitions[] = {
/*当前状态-当前事件-下个状态-动作*/
{State_A, Event_1, State_B, action1},
{State_A, Event_2, State_C, action2},
// ... 其他状态转换
};
/*根据当前状态和输入事件在定义的表里面查找相应状态转换*/
Transition* findTransition(State current_state, Event current_event) {
for (int i = 0; i < sizeof(transitions) / sizeof(transitions[0]); i++) {
if (transitions[i].current_state == current_state &&
transitions[i].current_event == current_event) {
return &transitions[i];
}
}
return NULL;
}
// 执行相应的操作
void action1() {
printf("Performing action 1.\n");
}
void action2() {
printf("Performing action 2.\n");
}
int main() {
/*状态和事件初始化*/
State current_state = State_A;
Event current_event = Event_1;
/*查找状态转换,并执行相应的操作*/
Transition* transition = findTransition(current_state, current_event);
if (transition != NULL) {
/*如果表中存在对应的数据,则更新状当前状态*/
current_state = transition->next_state;
/*执行转换动作*/
transition->action();
}
return 0;
}
三、状态模式
回主目录
使用面向对象的方式,将状态抽象为类,但在C语言中,并不能直接使用面向对象的语法来实现状态机。但是可以通过结构体和函数指针来模拟面向对象的思想,如下所示:
#include <stdio.h>
// 定义状态机结构体
typedef struct {
void (*state)();
void (*transition)(Event);
} StateMachine;
// 定义状态函数
void stateA(Event event);
void stateB(Event event);
void stateC(Event event);
// 定义状态机对象
StateMachine state_machine = {stateA, NULL};
// 定义状态转换函数
void transition(Event event)
{
state_machine.state(event);
}
// 定义状态函数的实现
void stateA(Event event)
{
if (event == Event_1)
{
printf("Performing action 1.\n");
state_machine.state = stateB;
}
else if (event == Event_2)
{
printf("Performing action 2.\n");
state_machine.state = stateC;
}
}
void stateB(Event event)
{
// 状态B的实现...
}
void stateC(Event event)
{
// 状态C的实现...
}
int main()
{
Event current_event = Event_1;
// 执行状态转换
transition(current_event);
return 0;
}
寄存器组封装
回主目录
为了程序在编写过程中更方便的驱动底层寄存器,可以将共通的部分用结构体组合起来。以STM32标准库里面的代码为例:GPIOB寄存器组封装
1.封装总线和外设基地址
/*外设基地址*/
#define PERIPH_BASE ((unsigned int) 0x40000000)
/*总线基地址*/
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
/*GPIO外设基地址*/
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
/*寄存器基地址,以GPIOB为例*/
#define GPIOB_CRL (GPIOB_BASE + 0x00)
#define GPIOB_CRH (GPIOB_BASE + 0x04)
#define GPIOB_IDR (GPIOB_BASE + 0x08)
#define GPIOB_ODR (GPIOB_BASE + 0x0C)
#define GPIOB_BSRR (GPIOB_BASE + 0x10)
#define GPIOB_BRR (GPIOB_BASE + 0x14)
#define GPIOB_LCKR (GPIOB_BASE + 0x18)
/*对单个寄存器进行操作,如GPIOB_BSRR*/
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+0)); /*将第0位输出低电平*/
2.封装寄存器列表
由于GPIOA~GPIOE都各有一组功能相同的寄存器,
只是地址不一样,可以将上述通过基地址用结构体整合起来。
/*使用结构体对GPIO寄存器组的封装*/
typedef unsigned int uint32_t; /*无符号32位变量*/
/*GPIO寄存器列表*/
/*因为C语言语法规定,结构体变量的存储空间是连续的,其中32位占4个字节,
GPIO寄存器列表的个寄存器也是连续的,所以可以如下编写*/
typedef struct{
uint32_t CRL;
uint32_t CRH;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t BRR;
uint32_t LCKR;
}GPIO_TypeDef;
3.通过结构体指针访问寄存器
设置完后,直接调用GPIOB即可设置其相关的寄存器组。
GPIO_TypeDef * GPIOx; /*定义一个GPIO_TypeDef型结构体GPIOx*/
GPIOx = GPIOB_BASE; /*把指针地址设置为宏GPIOB_BASE地址*/
GPIOx->IDR = 0xFFFF; /*设置GPIOB寄存器列表中的IDR寄存器*/
uint32_t temp = GPIOx->IDR; /*读取GPIOB寄存器列表中IDR寄存器的值*/
/*再合并结构体指针*/
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
GPIOB->IDR = 0xFFFF; /*设置GPIOB寄存器列表中的IDR寄存器*/
uint32_t temp = GPIOB->IDR; /*读取GPIOB寄存器列表中IDR寄存器的值*/
函数参数校验
回主目录
在C语言中,使用函数进行模块化代码设计是最常用的操作,可以让整个函数看起来更加的清晰明了,但函数的使用,就会存在函数参数调用的值与设想的值不同,从而导致细致的问题,而这种参数值问题,IDE是不怎么会报错的,就像判断语句中,常量在右边,==
写成了=
,就不会报错,但是逻辑存在问题。这样在函数编写时可用来参数检查,而在正式发布时就不需要这个功能。
1.定义USE_FULL_ASSERT宏来设置检验功能,打开检测功能
#define USE_FULL_ASSERT 1
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function which reports
* the name of the source file and the source line number of the call
* that failed. If expr is true, it returns no value.
* @retval None
*/
#ifdef USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */
2.声明参数范围
/** @defgroup GPIO_Exported_Types
* @{
*/
#define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \
((PERIPH) == GPIOB) || \
((PERIPH) == GPIOC) || \
((PERIPH) == GPIOD) || \
((PERIPH) == GPIOE) || \
((PERIPH) == GPIOF) || \
((PERIPH) == GPIOG))
3.参数检验
当函数输入的参数不是GPIOA~GPIOG时,会执行“assert_failed”函数,可在该函数中设置特定的错误打印,从而更快的定位错误位置。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
}
使用void强转类型实现共通
#include <stdio.h>
// 定义结构体
struct Student {
char name[20];
int age;
};
// 定义函数,接受void类型参数
void printStudent(void* student) {
// 将void指针转换为结构体指针
struct Student* s = (struct Student*)student;
printf("Name: %s, Age: %d\n", s->name, s->age);
}
int main() {
// 创建结构体变量并初始化
struct Student stu = {"Alice", 20};
// 调用函数并传递结构体变量作为参数
printStudent(&stu);
return 0;
}