[关闭]
@richey 2020-09-23T03:01:31.000000Z 字数 11123 阅读 826

物联网综合应用讲义(研究生)-02 嵌入式软件设计专题

物联网 讲义 嵌入式软件


1 嵌入式软件架构概述

1.1 经典的合作式调度器

考虑这样一个常见的系统:、

经典方法:前后台系统(超级循环)

问题

如何解决以上问题?

参考代码

  1. #include <reg51.h>
  2. #include "common.h"
  3. #define FOSC 11059200ul
  4. #define T0_H (65536-(10*FOSC)/(12*1000))/256
  5. #define T0_L (65536-(10*FOSC)/(12*1000))%256
  6. uint16_t tick = 0;
  7. bit bFlag1 = 0;
  8. bit bFlag2 = 0;
  9. bit bFlag3 = 0;
  10. bit bFlag4 = 0;
  11. void initSys();
  12. sbit LED0 = P1^0;
  13. sbit LED1 = P1^1;
  14. sbit LED2 = P1^2;
  15. sbit LED3 = P1^3;
  16. void main(){
  17. initSys();
  18. while(TRUE){
  19. if(bFlag1){
  20. bFlag1 = 0;
  21. LED0 = !LED0;
  22. }
  23. if(bFlag2){
  24. bFlag2 = 0;
  25. LED1 = !LED1;
  26. }
  27. if(bFlag3){
  28. bFlag3 = 0;
  29. LED2 = !LED2;
  30. }
  31. if(bFlag4){
  32. bFlag4 = 0;
  33. LED3 = !LED3;
  34. }
  35. }
  36. }
  37. void initSys(){
  38. TMOD = 0x01;
  39. TH0 = T0_H;
  40. TL0 = T0_L;
  41. EA = 1;
  42. ET0 = 1;
  43. TR0 = 1;
  44. }
  45. void timer0() interrupt 1 {
  46. TH0 = T0_H;
  47. TL0 = T0_L;
  48. tick++;
  49. if((tick % 100) == 0){
  50. bFlag1 = 1;
  51. }
  52. if((tick % 150) == 3){
  53. bFlag2 = 1;
  54. }
  55. if((tick % 20) == 5){
  56. bFlag3 = 1;
  57. }
  58. if((tick % 300) == 7){
  59. bFlag4 = 1;
  60. }
  61. }

1.2 长任务的处理:用状态机切割任务

新的问题?

有限状态机的概念:FSM

以下都是状态机程序其实就是状态机

状态机的要素

  1. 状态:是指当前所处的状态。
  2. 条件:又称为“事件”。当一个条件被满足,将会触 发一个动作,或者执行一次状态的迁移。
  3. 动作:条件满足后执行的动作。动作执行完毕后, 可以迁移到新的状态,也可以仍旧保持原状态。动 作不是必需的,当条件满足后,也可以不执行任何 动作,直接迁移到新状态。
  4. 次态:条件满足后要迁往的新状态
    image.png-48.9kB

image.png-273.3kB

! image.png-443.2kB

image.png-122.8kB

状态机的实现方法

  1. void Task1Function(void) {
  2. If(taskState==STATE1) {
  3. 执行动作1();
  4. taskState=STATE2;
  5. return;
  6. }
  7. If(taskState==STATE2) {
  8. 执行动作2();
  9. taskState=STATE3;
  10. return;
  11. }
  12. If(taskState==STATE3) {
  13. 执行动作3();
  14. taskState=STATE4;
  15. return;
  16. }
  17. If(taskState==STATE4) {
  18. 执行动作4();
  19. taskState=STATE1;
  20. return;
  21. }
  22. }
  1. void Task1Function(void) {
  2. switch(taskState) {
  3. case STATE1: {
  4. 执行动作1();
  5. taskState=STATE2;
  6. break;
  7. }
  8. case STATE2: {
  9. 执行动作2();
  10. taskState=STATE3;
  11. break;
  12. }
  13. case STATE3: {
  14. 执行动作3();
  15. taskState=STATE4;
  16. break;
  17. }
  18. case STATE4: {
  19. 执行动作4();
  20. taskState=STATE1;
  21. break;
  22. }
  23. default:{
  24. 执行动作5();
  25. taskState=STATE1;
  26. }
  27. }
  1. void fun1(void) {
  2. }

编译器会如何处理?(汇编代码 )
调用该函数的方法:

  1. fun1();
  2. void (*pFun1)() = fun1;
  3. //调用该函数的方法:
  4. (*pFun1)();
  5. //或
  6. pFun1();

用函数指针代替switch case语 句实现一个计算器
代码片段

  1. double caculate(int oper, doubleop1, double op2)
  2. switch( oper) {
  3. case ADD:
  4. result = add( op1, op2);
  5. break;
  6. case SUB:
  7. result = sub( op1, op2);
  8. break;
  9. case MUL:
  10. result = mul( op1, op2);
  11. break;
  12. case DIV:
  13. result = div( op1, op2);
  14. break;
  15. }
  16. }

对于一个新奇的具有上百个操作符的计算器,这条switch语句将非常长。
编译器的问题:可能会将多个case语句的代码编译为if/else 结构,当然也可能优化为函数指针结构。

  1. //使用函数指针实现:代码片段
  2. //函数声明
  3. double add doubledouble);
  4. double sub doubledouble);
  5. double muldoubledouble);
  6. double div doubledouble);
  7. ......
  8. double (*oper_func[ ] )( double, double)={ add,sub,mul,div,... };
  9. //用下面这条语句替换前面整条switch 语句!
  10. result = oper_func[oper](op1, op2);
  11. //或
  12. result = (*oper_func[ oper])(op1, op2);

状态机的函数指针实现:

  1. typedef void(*State)(Fsm *me, unsigned const sig);
  2. /*状态定义:函数指针,状态迁移后直接调用对应的处理函数*/
  3. struct Fsm{
  4. State state_;/* 状态机本身由函数指针实现*/
  5. };
  6. #define FsmDispatch(me_, sig_) ((*(me_)->state_)((me_), sig))
  7. /*状态分发,代替任务函数*/
  8. #define TRAN(target_) (((Fsm*) me)->state_ = (State)(target_)) /*状态迁移*/ /*状态函数(子任务)*/
  9. void FsmFunction1(Fsm *me, unsigned const sig) {
  10. switch (sig) {
  11. case SLASH_SIG:
  12. //do sth here;
  13. TRAN(FsmFunction2); /* 迁移到下一个状态*/
  14. break;
  15. }
  16. //...
  17. }

1.3 合作式调度器+有限状态机

image.png-73.1kB
嵌入式软件架构设计之 合作式调度器+有限状态机架构的实现方法

  1. main() {
  2. A_taskInit();//任务的初始化
  3. B_taskInit();
  4. ...
  5. while(1) {
  6. A_taskProc();//对于长任务,用状态机切割
  7. B_taskProc();
  8. }

关于延时的实现方法,以下为OS支持下任务的写法

  1. void xxxTask(void) {
  2. while(1){
  3. //waitEvent
  4. //do step_1
  5. /*操作系统延时,让出CPU*/
  6. rt_thread_sleep(rt_tick_t tick);
  7. rt_thread_delay(rt_tick_t tick);
  8. rt_thread_mdelay(rt_int32_t ms);
  9. //do step2
  10. }
  11. }
  12. void xxxTask(void) {
  13. static unsigned inttaskStat= STAT_GENERAL;//任务状态变量 static rt_tick_t startTick;
  14. static rt_tick_t tick currentTick;
  15. if (taskStat == STAT_GENERAL) {
  16. //check event
  17. //if no event
  18. return;
  19. //do step_1
  20. startTick = rt_tick_get(); //rt_tick_get()就是察看系统时间
  21. taskStat = STAT_WAIT;
  22. return;
  23. }
  24. else if (taskStat == STAT_WAIT) {
  25. currTick= rt_tick_get();
  26. if ((currTick-startTick) >= TIME_OUT_TICK_NUM) {
  27. //do step_2
  28. taskStat= STAT_GENERAL;
  29. return;
  30. }
  31. else return;
  32. }

1.4 举例:8通道A/D采集任务的状态化

方法:将8通道采集切割为8个子任务

  1. //不切割任务的方法
  2. uint8_t ad_chnnel;
  3. for(ad_chnnel = 0; ad_chnnel < 8; ad_chnnel++) {
  4. //AD转换,通道号ad_chnnel由for语句自动加1,连续转换8次
  5. temperature = read_ad(ad_chnnel);
  6. ad_result[ad_chnnel] = temperature;//存入数组
  7. }
  1. //切割任务的方法,每次只进行一次转换
  2. static uint8_t ad_chnnel = 0; //声明为全局变量
  3. if(ad_chnnel < 8) {
  4. temperature = read_ad(ad_chnnel);//AD转换,
  5. ad_result[ad_chnnel] = temperature;//存入数组
  6. ad_chnnel++;//通道号加1
  7. }
  8. else
  9. ad_chnnel = 0;

1.5 举例:生产者-消费者设计模式-以modbus协议的实现

image.png-87.6kB

1.6 如何处理μs级、时序要求严格的任务?

这种方法的主体思想是:利用一个独立的定时器,在其中断处理程序中根据状态的不同 ,动态的修改TCNTn的值,并执行相应状态的代码

  1. typedef void(*lfsm_func_t)(void);
  2. typedef struct {
  3. uint8_t delta_t;
  4. lfsm_func_t fsm_func;
  5. lfsm_t *next_fsm;//一定要形成一个环形链表
  6. }lfsm_t;
  7. //初始化
  8. lfsm fsm_18B20[]= { …如何初始化? }
  9. void timer0_ISR(void) {
  10. static lfsm_t * current_fsm = fsm_18B20;
  11. lfsm_t *temp_fsm = current_fsm;
  12. TCNT0 = current_fsm->delta_t;//修改到达下一次中断的时间
  13. current_fsm = current_fsm->next_fsm;
  14. temp_fsm->fsm_func();//调用状态机
  15. }

1.7 使用实时操作系统(RTOS)

在RTOS环境下,一般提供抢占式调度。RTOS环境下的任 务,一般处于一个while(1)循环中。

  1. while(1){
  2. 从消息队列接收消息。如果没有,将阻塞。 处理消息。

2 嵌入式软件设计之编码规范

2.1 排版

  1. 程序块要采用缩进风格编写,缩进的空格数为4个。
  2. 较长的语句(>80字符)要分成多行书写,长表达式要在低 优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
  3. 一行只写一条语句。
  4. 函数或过程的开始、结构的定义及循环、判断等语句中的代 码都要采用缩进风格,case语句下的情况处理语句也要遵从 语句缩进要求。
  1. //范例1:
  2. void Fun1(int iCh) {
  3. intI,j,iTmp;
  4. i=0;
  5. if(iCh<8) {
  6. GetAdValue();
  7. }
  8. }
  9. //范例2:
  10. void Fun2(int iCh)
  11. {
  12. switch(iCh)
  13. {
  14. case 0:
  15. GetAdValue(iCH);
  16. break;
  17. case 1:
  18. }
  19. }

2.2 注释

  1. 源程序有效注释量一般在10%-20%。
  2. 注释应当准确、易懂,防止注释有二义性。
  3. 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
  4. 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
  5. 留白,占代码量的20%

2.3 命名规则

关于命名的风格,应用比较广的有三种。

那么,该选用哪种命名风格呢?建议是“取百家之长”,混用这几种中能够让名字辨识度最高的那些优点:

  1. #define MAX_PATH_LEN 256 //常量,全大写
  2. #define BUAD_RATE 19200
  3. int g_sys_flag; // 全局变量,加g_前缀
  4. float g_currentValue;
  5. //类型定义加_t后缀
  6. typedef struct {
  7. WSAOVERLAPPED ovlp;
  8. ngx_event_t *event;
  9. int error;
  10. } ngx_event_ovlp_t;
  11. //结构体定义加_s后缀
  12. struct ngx_chain_s {
  13. ngx_buf_t *buf;
  14. ngx_chain_t *next;
  15. };
  16. namespace linux_sys { // 名字空间,全小写
  17. void get_rlimit_core(); // 函数,全小写
  18. }
  19. class FilePath final // 类名,首字母大写
  20. {
  21. public:
  22. void set_path(const string& str); // 函数,全小写
  23. private:
  24. string m_path; // 成员变量,m_前缀
  25. int m_level; // 成员变量,m_前缀
  26. };

命名另一个相关的问题是“名字的长度”,一个被普遍认可的原则是:变量 / 函数的名字长度与它的作用域成正比,也就是说,局部变量 / 函数名可以短一点,而全局变量 / 函数名应该长一点。

3 嵌入式软件设计之C语言编程要点

3.1 C语言的几个关键字

  1. define 用预处理指令#define声明一个常数

    • #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)ul
    • #define 语法的基本知识(不能以分号结束,括号的使用)
    • 书写注意可读性。
    • 意识到这个表达式将使一个16 位机的整型数溢出,因此要用 到长整型符号l,告诉编译器这个常数是的长整型数。
    • 如果你在你的表达式中用到ul(表示无符号长整型),那 么你有了一个好的起点。
    • 写一个"标准"宏MIN
      #define MIN(A,B) ((A)<= (B) ? (A) : (B))
    • 使用宏嵌入代码。对于嵌入式系统来说,为了能达到要求 的性能,嵌入代码经常是必须的方法。(思考:与函数相 比的优点,另外一个嵌入代码的方法是使用inline关键字声明 内联函数)
    • 三重条件操作符,产生比if-then-else 更优化的代码。
    • 宏中参数用括号括起来
    • 宏的副作用,例如:
    • least = MIN(*p++, b); //思考:有什么副作用?
  2. static

    • 第1种用法:函数体内的static,供函数使用的全局变量
  1. void fun1(void) {
  2. static int counts= 0 ;
  3. counts++;
  4. }
  5. fun1();
  6. fun1();
  7. fun1();
  1. static int counts2;
  2. void fun1(void) {
  3. counts2= 0 ;
  4. counts2++;
  5. }
  6. void fun2(void) {
  7. counts2++;
  8. }
  9. fun1();
  10. fun2();
  1. static void fun1(void) {
  2. counts3= 0 ;
  3. counts++;
  4. }
- 作用:仅供本模块函数调用
- 以上第2,3种用法: 本地化数据和代码范围的好处和重要性

3. const

  1. const int tmp;
  2. int const tmp;
  3. const int* ptr1; //问题:pData值是否可变?
  4. int* const ptr2;
  5. //问题:ptr2值是否可变?
  6. int const * ptr3 const;//问题:ptr3值是否可变?

思考:编译器如何处理?地址分配在什么地方?
使用const关键字的好处:
- 给读你代码的人传达非常有用的信息.
- 给优化器一些附加的信息,能产生更紧凑的代码。
- 使编译器很自然地保护那些不希望被改变的参数 ,防止其被无意的代码修改。
4. volatile

5.typedef:类型定义

  1. #define mys_s struct s*
  2. typedef struct s* mys_t;
  3. mys_s p1,p2;
  4. mys_t p3,p4;

以上有什么区别? typedef的作用
6. extern 声明外部变量 跨模块的全局变量

  1. moudule1.c
  2. int iState;
  3. moudule2.c
  4. extern int iState;

变量(函数)声明与定义的区别:
声明:不分配存储空间,定义:分配存储空间 以下那个是申明,那个是定义?

  1. extern int x = 1024;
  2. int y;
  3. extern void reset (void *p) {}
  4. extern const int *pi;
  5. void print(const matrix &);
  1. 内嵌汇编 __asm
  1. int result;
  2. void Add(long a, long *b) {
  3. __asm{
  4. MOV AX, a
  5. MOV BX, b
  6. ADD AX, [BX]
  7. MOV result, AX
  8. }
  9. }
  10. __asm void wait() {
  11. nop
  12. nop
  13. nop
  14. nop
  15. nop
  16. nop
  17. BX lr
  18. }

3.2 C语言编程要点

3.2.1 访问固定内存

  1. int *ptr;
  2. ptr = (int *)0x67a9;
  3. *ptr = 0xaa55;

一个较晦涩的方法是:

  1. *(int * const)(0x67a9) = 0xaa55;

3.2.2 关于中断(__interrupt)

  1. #define PI (3.1415926)
  2. float EXTI0_IRQHandler(float fRadius ){
  3. double area = PI * fRadius * fRadius ; //
  4. printf("\nArea = %f", area);
  5. return area;
  6. }

总的来说,如果一个函数在重 入条件下使用了未受保护的共
享的资源,那么它是不可重入 的。 示例:

  1. int tmp;
  2. void func1(int* x, int* y) {
  3. tmp = *x;
  4. *x = *y;
  5. *y = tmp;
  6. }
  7. void swap_char2(char *lpcX, char *lpcY) {
  8. static char cTemp;
  9. cTemp = *lpcX;
  10. *lpcX = *lpcY;
  11. lpcY = cTemp;
  12. }
  13. void func2(int* x, int* y) {
  14. int tmp;
  15. tmp = *x;
  16. *x = *y;
  17. *y = tmp;
  18. }

共享数据的问题(中断的数据保护)

  1. static int temperatures[2];
  2. void main(void) {
  3. int temp0,temp1;
  4. while(TURE){
  5. temp0 = temperatures[0];
  6. temp1 = temperatures[1];
  7. if(iTemp0 != iTemp1){
  8. ...
  9. }
  10. }
  11. }
  12. void interrupt reead_temperatures(void) {
  13. temperatures[0] = ??;//从硬件读取温 度值
  14. ??//
  15. temperatures[1] = ??;//从硬件读取温 度值
  16. }

思考:如果第1条赋值语句被中断会发生什么?
解决方案:

  1. disable_interrupt;
  2. temp0 = temperatures[0];
  3. temp1 = temperatures[1];
  4. enable_interrupt;

共享数据的问题(中断的数据保护)
以下代码有什么问题?如何修改?

  1. static int iSeconds , iMinutes , iHours;

image.png-506.4kB

3.2.3 模块划分

  1. /*module1.h*/
  2. int a = 5; /* 在模块1的.h文件中定义int a */
  3. /*module1 .c*/
  4. #include "module1.h" /* 在模块1中包含模块1的.h文件 */
  5. /*module2 .c*/
  6. #include "module1.h" /* 在模块2中包含模块1的.h文件 */
  7. /*module3 .c*/
  8. #include "module1.h" /* 在模块3中包含模块1的.h文件 */

以上代码有什么问题?
正确的方法

  1. /*module1.h*/
  2. extern int a; /* 在模块1的.h文件中声明int a */
  3. /*module1.c*/
  4. #include "module1.h" /* 在模块1中包含模块1的.h文件 */
  5. int a = 5; /* 在模块1的.c文件中定义int a */
  6. /*module2 .c*/
  7. #include "module1.h" /* 在模块2中包含模块1的.h文件 */
  8. /*module3 .c*/
  9. #include "module1.h" /* 在模块3中包含模块1的.h文件 */

划分模块的原则:以下是最简单的划分方法 一个嵌入式系统通常包括两类模块:
1. 硬件驱动模块
一种特定硬件对应一个模块;
一个硬件驱动模块通常应包括如下函数:

2.软件功能模块
其模块的划分应满足低耦合、高内聚的要求。
例如:主程序1个模块

如何避免重复引用头文件:

  1. module1.h
  2. #ifndef _module1_h
  3. #define _module1_h
  4. ....//头文件具体内容
  5. #endif
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注