原课程链接: 嵌入式开发系统学习路线 从基础到项目 精品教程 单片机工程师必备课程 物联网开发 c语言 2022追更

前言

在学习过程中,老师提到了一个很重要的思想:主要从学习嵌入式的角度学习各项技能。比如c语言,语法有很多,但是其中可能有的在嵌入式开发中并不常用。如果是抱着高中备考的态度去学,搞懂每一个知识点,会浪费大量的精力。而且适用相关方面有基础的人。因此,如果读者把其中某一部分拿出来单独学习(比如为了学习c语言而学习C语言章节)并不合适。

像以往一样,我只是整理了供自己学习的学习笔记,希望读者多多支持原作者~ 也希望我的理解对你有帮助。

C 语言开发基础

本章节主要学习与嵌入式开发相关的 C语言基础知识。最终成果期望能做出 Zlog 的日志框架和 google test 的自测框架。

基本数据类型

char short int。

unsigned char: 0x00~0xff

unsigned short: 0x0000~0xffff

unsigned int: 0x00~0xffff ffff.

要熟悉一些类型的转换。

应用:比如Unix时间戳,可以转换为8位的16进制数(如:AAAA AAAA)。服务器可能发送一个一维长度为4的 unsigned char 数组给我们,我们要通过移位运算合并。

unsigned char time_stamp_arr[4]=[0XAA,0XAA,0XAA,0XAA];
unsigned int time_stamp=(((((time_stamp_arr[0]<<8)|time_stamp_arr[1])<<8)|time_stamp[2])<<8)|time_stamp[3];

判断

判断当然非常常用。点灯可能就用的到判断。

主要就是 if 和 switch 的语法。多判断当然更推荐 switch。

循环

遍历当然用途也很广。比如一直循环查看有无新订单。

for 和 while 的语法。

static

首先,static 可以限制作用域。被 static 限制的函数不能被其他文件使用。除非这个函数在同一个文件中被非 static 的函数引用了,然后别的文件引用那个非 static 的函数来间接引用这个函数。

vscode 怎么运行多个源代码文件?比如要运行 Main.c,Main.c 中引用了 PrintMessage.c 中的内容,就可以输入gcc Main.c PrintMessage.c -o Main来链接多个源代码文件并生成可执行文件Main.exe. 注意这里没有 -c,-c 后跟多个文件会报错。

然后进行本例的测试。我在 PrintMessage.c 中这样写:

#include<stdio.h>
#include"PrintMessage.h"
void printMessage(){printMessage1();
}static void printMessage1(){printf("hello world");
}

在 PrintMessage.h 中这样声明:

#ifndef _PRINTMESSAGE_H
#define _PRINTMESSAGE_H
#include<stdio.h>
void printMessage();
void printMessage1();
#endif

注意这里如果 printMessage1() 声明也是 static 类型,Main 就会报错直接找不到这个函数。如果 .h 声明中没有 static 关键字,Main 会正常提示:这个函数是 static 的,你别的文件用不了。但是通过 printMessage() 间接调用 printMessage1() 是可以的。

static 还可以声明变量,被 static 修饰的变量指挥被定义一次,下次访问时仍然继承上次的值。比如我们要统计一台坏掉的打印机被访问了几次:

void printMessage(){static int i=0;printf("i=%d\n",i++);
}

当这个函数被 Main.c 访问5次,输出并不是00000每次重新定义,而是01234值一直继承。

define typedef

就是同内容替换。比如区分测试和发布版本,或者开启关闭日志功能可能有用。

比如:

#define LOG_OPEN 1//这一句没有被注释,才能输出当前版本信息。注释掉了就说明关闭日志了,不会开启选择结构
#define VERSION_PRE 1
//#define VERSION_POST 1
//#define VERSION GLOD 1#ifdef LOG_OPEN#ifdef VERSION_PRE printf("This is a pre version!\n");
#ifdef VERSION_POST printf("This is a post version!\n");
#elif VERSION_GLOD//正式版printf("This is a glod version!\n");
#endif

通过这种方式首先选择开启还是关闭日志。如果开启,区分是测试版,发布版还是正式版,并执行相应的代码。发布的时候我们就可以直接修改 define 发布。

define 和 typedef 的区别:define 就直接在原文中替换了,typedef 是别名的意思。比如:

#define PCHAR1 char *
PCHAR1 c1,c2;//相当于:char *c1, c2; 实际上 c1 是指针,c2 是 char 类型。typedef char * PCHAR2
PCHAR2 c3,c4;//相当于:char *c1;char *c2;

typedef 常用于类型定义,比如比较经典的单片机定义:unsigned char => u8, unsigned short => u16, unsigned int =>u32

enum

比如我们要确认当前网络连接状态,可能有四种:初始化,连接中,连接成功,连接失败。我们可以给这四种状态定义值=1 2 3 4。

int net_status=2;
switch(net_status){case 1:printf("NETWORK INITING");break;case 2:printf("NETWORK CONNECTING");break;case 3:printf("NETWORK CONNECT SUCCESS");break;case 4:printf("NETWORK CONNECT FAIL");break;default:printf("ERROR OCCURRED!\n");break;
}

1 2 3 4 挺难理解的。我们就可以用上刚刚学到的 #define,赋予 1 2 3 4 意义。

#define NET_CONNECT_INIT 1
#define NET_CONNECT_ING 2
#define NET_CONNECT_SUCCESS 3
#define NET_CONNECT_FAIL 4
int net_status=NET_CONNECT_ING;
switch(net_status){case NET_CONNECT_INIT:printf("NETWORK INITING");break;case NET_CONNECT_ING:printf("NETWORK CONNECTING");break;case NET_CONNECT_SUCCESS:printf("NETWORK CONNECT SUCCESS");break;case NET_CONNECT_FAIL:printf("NETWORK CONNECT FAIL");break;default:printf("ERROR OCCURRED!\n");break;
}

这样可读性高多了。但是如果能给 status 加些限制就更好了,让其值只能从4种 define 中选择。

typedef enum{NET_CONNECT_INIT,NET_CONNECT_ING,NET_CONNECT_SUCCESS,NET_CONNECT_FAIL,
}E_NET_STATUS;

struct

自己定义一种结构类型。比如一个学生有姓名年龄性别等属性。一台售货机有各种传感器、各种设备的属性。定义为一个结构体方便管理。

typedef struct {/* data */char *name;int age;enum{f,m,} sex;
}Student;
void main(){Student s1;s1.age=19;s1.sex=f;s1.name="girl";printf("s1.name=%s",s1.name);
}

指针

普通变量是为了表示数据而生,指针变量是为了传递数据而生。指针变量指明了一个变量数据的地址,所有人拿到那个地址之后都可以去那个地址找那个变量读写,而不像普通的函数传参,得到的变量只是复制了一份,修改也不是修改的原来的变量。

比如最经典的 swap 函数,直接传普通的变量原变量值并不会被修改。

void swap(int *a, int *b){int temp=*a;*a=*b;*b=temp;
}
int main(){int a=10;int b=20;swap(&a,&b);printf("a=%d\nb=%d\n",a,b);
}

同类型的多个数据可以用数组存储。

回调函数

菜鸟教程上的使用例:

#include<stdio.h>int Callback_1(int x) // Callback Function 1
{printf("Hello, this is Callback_1: x = %d ", x);return 0;
}int Callback_2(int x) // Callback Function 2
{printf("Hello, this is Callback_2: x = %d ", x);return 0;
}int Callback_3(int x) // Callback Function 3
{printf("Hello, this is Callback_3: x = %d ", x);return 0;
}int Handle(int y, int (*Callback)(int))
{printf("Entering Handle Function. ");Callback(y);printf("Leaving Handle Function. ");
}int main()
{int a = 2;int b = 4;int c = 6;printf("Entering Main Function. ");Handle(a, Callback_1);Handle(b, Callback_2);Handle(c, Callback_3);printf("Leaving Main Function. ");return 0;
}

可见,回调函数的一个特点是耦合性低。针对不同的回调函数我们使用同一个函数接口。

另一个特点:类似中断处理,可以在发生某事件时主动调用该函数。

比如,我们想让自动售货机货空的时候发消息提示我们:没有货物啦。要补货。怎样实现?

第一种实现方式:我们写一个循环 while(1) 函数,在里面一直一直判断if(good_num==0),变为0的时候输出信息“要补货”。

死循环好吗,不好。浪费系统资源。

第二种实现方式:我们不必被动的一遍遍检查当前货物有没有=0,我们可以在货物=0时主动调用回调函数输出信息“要补货”。在货物=0时调用 Handle 函数,传入回调函数指针和货物数:0,主动访问该函数。

数据结构基本概念

有时候我们可能需要做数据拷贝。

malloc 函数裸机不建议使用。因为无法利用碎片化空间,只能直接申请一整块空间。带操作系统的计算机可以。

为了实现数据拷贝,我们还是有必要了解一些数据结构的。

数组:批量的同类型数据。

栈:比如程序执行了一半,一个中断进来了,我们要把原来执行了一半的程序存储到栈里。栈最主要的特点就是先进后出。

队列:先入先出。比如批量处理用户的订单。

链表:可以插队,比如有vip用户要先处理,插入队首。并且只要知道链表首地址就知道整个链表所有元素的地址。

树:树的排列一般是由特征的,可以找到相应特征的元素,比如二叉树找最大的。

实战1:Zlog 日志框架

下面做一个简单的项目,一个低配版 Zlog 日志框架。

首先,日志会输出一些信息,这自然不用多说。但是并不是所有情况下我们都希望日志信息输出的,比如发布版和正式版我们不希望用户看到日志信息。

解决方法很简单,之前在 define 部分也有学习到。就是在文件开头定义当前文件所处的模式,下面通过ifdef来判断现在所处的模式,进而决定要不要输出日志文件。

#ifdef OPEN_LOG
printf();
printf();
printf();
#endif

我们可以把这一部分封装到函数里。C 语言有一种变参函数,支持传入不同长度的参数。是 stdarg.h 下的 type va_arg(va_list ap, type) 函数。

菜鸟教程上的实例程序如下:

#include <stdarg.h>
#include <stdio.h>int sum(int, ...);int main()
{printf("15 和 56 的和 = %d\n",  sum(2, 15, 56) );return 0;
}int sum(int num_args, ...)
{int val = 0;va_list ap;int i;va_start(ap, num_args);for(i = 0; i < num_args; i++){val += va_arg(ap, int);//va_arg返回的是不固定的参数部分,固定的部分就是num_args,不用通过这个函数获取了。}va_end(ap);return val;//因此最后获取到的 val 值只是不固定的参数的求和,即15+56
}

传入的参数们 num_args 通过 va_start函数初始化成一个va_list类型的对象 ap,ap 通过va_arg(va_list, 数据类型)遍历。

因此我们可以如法炮制,写一个print_log函数,传入一串不固定长度的要输出的参数。首先要在函数里用ifdef判断一下当前是否是日志模式。

如果是日志模式,则可以继续输出,先利用va_start初始化 va_list 列表,再逐个获取参数并输出。

#include<stdio.h>
#include<stdarg.h>
#define OPEN_LOG 1void printLog(int num_args,...){//这里的第一个参数也不一定非要是 num_args,这里只是一种用法,方便知道传入了几个参数。#ifdef OPEN_LOGva_list ap;int i=0;va_start(ap,num_args);for(;i<num_args;i++){char * str=va_arg(ap,char *);printf("\t%s\n",str);}va_end(ap);#endif
}int main(){printf("Log:\n");printLog(3,"123","123","789");return 0;
}

实际上,我们学习C语言的第一个程序 hello world 里面就有一个变参函数: pintf。其中第一个参数是固定的 char * 字符串,后面的参数都是替换字符串中的类型符的参数。

printf 是直接把内容输出到屏幕,sprintf(char *str, const char * format, ...) 是将 format 字符串输出到 str 字符串中,后面的可变参数是用来替换 format 字符串中的类型符的。而 snprintf(char *str, size_t size, const char *format, ...) 是限定输出最大 size 长度。而 vsnprintf 功能与 snprintf 完全一致,只是把参数列表替换为 va_list 类型。

因此利用这两种函数,我们可以不直接输出日志字符串,而是先将要打印的信息输出到变参函数中,在函数中利用 vsnprintf 将要打印的信息合并成一整个字符串,再进行输出。

#include<stdio.h>
#include<stdarg.h>
#define OPEN_LOG 1void printLog(char *fmt,...){#ifdef OPEN_LOGva_list arg;va_start(arg,fmt);char str[1+vsnprintf(NULL,0,fmt,arg)];//通过这条语句获取 fmt 的长度并定义相应长度的字符数组vsnprintf(str,sizeof(str),fmt,arg);//vsprintf 自然也可以,vsnprintf 保险一些printf("%s",str);va_end(arg);#endif
}int main(){printf("Log:\n");printLog("\t%s\n\t%s\n\t%s\n","123","123","789");return 0;
}

接下来是限制不同版本的日志输出信息。比如我们设置四种日志输出等级:debug, info, warn. error。我们希望输出等级是层层递增的,比如调试版本四种信息都会输出,正常版本只会输出 info, warn, error 信息,发布版本只会输出 error 信息。用之前 define 章节学到的版本定义非常容易实现。

#define OPEN_LOG 1//设置开启日志//设置当前日志版本
#define LOG_LEVEL LOG_DEBUG
// #define LOG_LEVEL LOG_INFO
// #define LOG_LEVEL LOG_WARN
// #define LOG_LEVEL LOG_ERRORtypedef enum{LOG_DEBUG=0,LOG_INFO,LOG_WARN,LOG_ERROR,
}E_LOGLEVEL;//四个值分别是0 1 2 3 4//printLog 函数中做一些改动。首先开头不只是一个固定参数,而是两个固定参数,第一个是版本号,比如是 LOG_ERROR 就代表这条信息是 ERROR 等级下要输出的。第二个代表 fmt 原字符串。
//然后要 printf 输出之前先判断一下当前版本与要输出的信息的版本。比如当前版本是 INFO 版本,要输出的信息是 LOG_DEBUG 的信息,那么这条信息就不用输出。
void printLog(const int level, const char *fmt,...){#ifdef OPEN_LOGva_list arg;va_start(arg,fmt);char str[1+vsnprintf(NULL,0,fmt,arg)];vsnprintf(str,sizeof(str),fmt,arg);if(level>=LOG_LEVEL)//注意看,这句是关键printf("%s",str);va_end(arg);#endif
}int main(){printf("Log:\n");printLog(LOG_DEBUG,"\t%s VERSION MESSAGE: %s\n","LOG_DEBUG","123");printLog(LOG_INFO,"\t%s VERSION MESSAGE: %s\n","LOG_INFO","456");printLog(LOG_WARN,"\t%s VERSION MESSAGE: %s\n","LOG_WARN","789");printLog(LOG_ERROR,"\t%s VERSION MESSAGE: %s\n","LOG_ERROR","012");return 0;
}

这样执行输出的会是四条语句。也可以切换当前日志等级。

在调试过程中,输出一些代码相关信息如当前所处函数、当前所处行数等。

void printLog(const int level, const char * function, const int line, const char *fmt,...){#ifdef OPEN_LOGva_list arg;va_start(arg,fmt);char str[1+vsnprintf(NULL,0,fmt,arg)];vsnprintf(str,sizeof(str),fmt,arg);if(level>=LOG_LEVEL)printf("Function: %s; Line: %d\n%s",function, line, str);va_end(arg);#endif
}
#define PRINT_LOG(level, fmt...) printLog(level, __FUNCTION__, __LINE__, fmt)

在整个的 printLog 函数结尾还可以增加一个状态:日志已保存 isSaved,代表整个字符串已经顺利写入 str 缓冲数组中。

最后一步就是把所有的函数声明、define、typedef 提出来放到 .h 文件中,方便管理。

实战2:google 测试框架

随着需求不断变化,版本也会不断更迭,难免会对一些函数做修改。

如果对函数做的操作使得函数违背了最初期望实现的功能呢?应该增加一些限制,比如修改完函数后对函数进行测试。如果结果符合预期,再保留修改。

比如写一个最简单的 sum 函数进行两数求和,我们可以通过测试函数比较输入 a b 后函数的返回值和 a+b 的值是否相等。

#include<stdio.h>
#include<stdlib.h>typedef struct{int a;int b;int output;int (*TestEMFunc)(int,int);//这里代表一个有两个 int 参数的 返回值为 int 类型的函数
}GTest;GTest *addFunc(int (*TestEMFunc)(int,int),int a,int b, int output){//这里传入的 output 是程序员自己给的,期望的结果。GTest *m_EMTest=(GTest *)malloc(sizeof(GTest));m_EMTest->a=a;m_EMTest->b=b;m_EMTest->TestEMFunc=TestEMFunc;m_EMTest->output=output;return m_EMTest;
}int add(int a, int b){return a+b;}void runGTest(GTest *gTest){if(gTest!=NULL){int temp=gTest->TestEMFunc(gTest->a, gTest->b);//这里 temp 计算得出的结果是函数的返回值。if(temp==gTest->output)printf("测试成功!\n");else printf("测试失败!建议重新修改函数\n");}
}void main(){GTest *gTest=addFunc(add, 2, 3, 5);runGTest(gTest);return;
}

当然这只是最基础的一个形式。github 上有 google 测试框架,把此例弄懂后去理解 google 测试框架会更加容易。

小智学长嵌入式入门学习路线_1 C语言基础相关推荐

  1. 0. 嵌入式入门学习路线

    最近有好多同学在咨询嵌入式该怎么入门,应该怎么学习,有什么好的学习方法推荐,以及嵌入式入门的学习路线.今天我就带着大家的问题,一一为大家解决. STM32基础入门 uCOS-II基础入门 文末有相关学 ...

  2. 《嵌入式入门学习第一阶段——C语言》

    嵌入式学习第一阶段(今日总结)2021/9/7 1.C语言的基础框架 #include<stdio.h> //预编译int main() //入口函数 {···return 0; //函数 ...

  3. python语言的考试_【Python学习路线】Python语言基础自测考试 - 中级难度

    {"moduleinfo":{"question":"//developer.aliyun.com/ask/new","right ...

  4. 嵌入式新手学习路线,嵌入式课程学习课程分享

    嵌入式开发就是指在嵌入式操作系统下进行开发,一般常用的系统有WinCE,ucos,vxworks,linux,android等.另外,用c,c++或汇编开发:用高级处理器,arm7,arm9,arm1 ...

  5. 嵌入式新手学习路线,嵌入式课程学习

    嵌入式开发就是指在嵌入式操作系统下进行开发,一般常用的系统有WinCE,ucos,vxworks,linux,android等.另外,用c,c++或汇编开发;用高级处理器,arm7,arm9,arm1 ...

  6. python速成要多久2019-8-28_2019最全Python入门学习路线,不是我吹,绝对是最全

    近几年Python的受欢迎程度可谓是扶摇直上,当然了学习的人也是愈来愈多.一些学习Python的小白在学习初期,总希望能够得到一份Python学习路线图,小编经过多方汇总为大家汇总了一份Python学 ...

  7. 人工智能新手入门学习路线!附学习资源合集

    有段时间没跟大家分享编程资源福利了!今天为大家整理了人工智能新手入门学习路线,同时附700分钟的学习资源合集,相信这套福利可以帮你顺利入行AI!文末领取全部资料. 一.AI基础好课学习资料整理(约31 ...

  8. 自学python推荐书籍2019-2019最全Python入门学习路线,不是我吹,绝对是最全

    近几年Python的受欢迎程度可谓是扶摇直上,当然了学习的人也是愈来愈多.一些学习Python的小白在学习初期,总希望能够得到一份Python学习路线图,小编经过多方汇总为大家汇总了一份Python学 ...

  9. 自学python推荐书籍2019-2019最全Python入门学习路线,绝

    近几年Python的受欢迎程度可谓是扶摇直上,当然了学习的人也是愈来愈多.一些学习Python的小白在学习初期,总希望能够得到一份Python学习路线图,小编经过多方汇总为大家汇总了一份Python学 ...

最新文章

  1. mysql主从配置流程
  2. 【Python】调用百度云API驾驶行为分析 Driver Behavior
  3. python PyQt5 QLCDNumber类(用于显示数字或一些符号的容器)
  4. 07/08_flink shell,基本原理及应用场景、特点、架构图、集群解剖、JobManager、TaskManagers、tasks和操作链、Session/job集群、组件介绍等、应用场景
  5. 从“等等”到“秒开”再到“直开”,是什么让闲鱼社区相见恨晚?
  6. SQL注入——SQLmap的进阶使用(十五)
  7. jquery如何获取checkbox的值
  8. 昇思MindSpore全场景AI框架 1.6版本,更高的开发效率,更好地服务开发者
  9. KETTLE调度第三篇:Windows下调度Dos脚本编写和遇到的一些问题解决
  10. SQLserver通过链接服务器连接oracle
  11. 强化学习算法面试问题 解答
  12. 80386汇编_全局描述表GDT介绍
  13. 载波聚合或双连接的方式进行_一文读懂5G基站和4G基站如何协同工作
  14. 【Only one connection receive subscriber allowed with】
  15. Android平台上使用气压传感器计算海拔高度
  16. 计算机系统维护工作内容
  17. python 线程(1)-- 常用方法与属性,锁,同步
  18. 26个顶尖战略咨询公司常用分析模型详解!
  19. obj文件和mtl文件格式说明
  20. unbound部署DNS

热门文章

  1. 6.7 交互式动态查询窗体
  2. unixbench图形化测试_unixbench使用方法
  3. 云计算:程序员重回个人英雄时代,国内云计算平台即将搭建运行。
  4. 【一个简单的vue公式编辑器组件】
  5. 字体大小中大写字号与小写字号对应关系
  6. 微课微视频制作软件CS(Camtasia Studio)完整学习视频教程-黄波-专题视频课程
  7. Linux系统启动管理
  8. Mondrian vs Elasticsearch:为您的项目选择什么
  9. unity中dds文件不可被识别,需要改成png格式。
  10. excel将数字变成字符串(一般excel存储数据都是科学计数的形式存储)