当前位置: 首页 > news >正文

建立对单片机/嵌入式启动、运行的整体认知

文章目录

  • 一、51单片机的启动过程
  • 二、STM32的完整启动流程分析
      • 1. 根据boot引脚决定三种启动模式
      • 2. 启动后bootloader做了什么?
      • 3. bootloader中对内存的搬移和初始化
      • 4. ISP、IAP、ICP三种烧录方式
      • 5. 参考资料
    • 从上电到启动,一文读懂STM32启动全流程
      • 1、直接上电?不,上电前还暗藏玄机
      • 2、Bootloader
  • 三、VxWorks的启动过程
  • 四、操作系统(RTOS)的启动流程是怎样的?
  • 五、Linux 系统启动流程
    • 引言
    • 第一阶段:硬件引导启动阶段
    • 第二阶段:BootLoader 启动引导阶段
    • 第三阶段:内核引导阶段
    • 第四阶段:Sys V init 初始化阶段
    • 第五阶段:启动完成
  • 六、STM32单片机是如何软硬件结合的
  • 七、单片机驱动开发
  • 八、从单片机步入Linux驱动开发(概念和Demo)
  • 九、从VxWorks的角度来看控制硬件
  • 十、实际项目中,怎么将开发板和各种硬件交联起来
  • 十一、总结

一、51单片机的启动过程

已剪辑自: https://blog.csdn.net/u013083059/article/details/62232920

接触单片机有几年的时间了,一直专注于如何在单片机上写一些应用,对单片机如何启动的知之甚少,惭愧惭愧。。。今天得空整理了一下,加深了对单片机的认识,如为什么定义data区里的变量重新开机的初始值为0。

单片机在开机上电后,会执行startup.A51文件的指令,我分析了一下某个项目中这个文件里的指令,在这里单片机会做如下几件事情:

  1. 初始化8051硬件堆栈的大小和堆栈指针;
  2. 初始化中断向量表,分配每个中断的入口地址和中断服务函数;
  3. 初始化内部RAM空间,即DATA/IDATA,将内容清零;
  4. 初始化外部RAM空间,即XDATA/PDATA,将内容清零;
  5. 初始化SMALL/COMPACT/LARGE模式下reentrant函数使用的堆栈指针;
  6. 调用main()函数,去执行我们编写的代码。

当用keil作为开发环境,创建一个工程时,需要选择所使用的单片机型号,然后Keil会将相应单片机的startup.A51文件拷贝到工程目录下,在编译时,该文件会被编译到最终的目标文件中。一般情况下,这个文件是不需要我们做修改的,保持默认状态即可,所以可能很多人对此文件不太熟悉。下面是具体的code以及我的个人分析:

$NOMOD51   ;取消8051对SFR的预定义,由用户自行定义。

; 以下定义3个SFR
sfr CLKSEL  = 0x8F
sfr P3 = 0xB0
sfr MMU_SEL = 0xC3

; 以下初始化IDATA, XDATA和PDATA存储区
IDATASTART  EQU 00H     ; the absolute start-address of IDATA memory
IDATALEN    EQU 100H        ; the length of IDATA memory in bytes.
;
XDATASTART  EQU 0H  ; the absolute start-address of XDATA memory
XDATALEN    EQU 0F00H   ; the length of XDATA memory in bytes.
;
PDATASTART  EQU 0H  ; the absolute start-address of PDATA memory
PDATALEN    EQU 100H    ; the length of PDATA memory in bytes.

; 定义存储PLL值的地址。
PLLADDR     EQU 0xEFFF
;

;  当函数是可重入的(用reentrant关键字修饰),以下初始化可重入函数所使用的堆栈, 考虑到了三种编译模式SMALL/COMPACT/LARGE。
;  Stack Space for reentrant functions in the SMALL model.
IBPSTACK    EQU 0   ; set to 1 if small reentrant is used.
IBPSTACKTOP EQU 0FFH+1  ; set top of stack to highest location+1.
;
;  Stack Space for reentrant functions in the LARGE model.  
XBPSTACK    EQU 0   ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 0FFFFH+1; set top of stack to highest location+1.
;
;  Stack Space for reentrant functions in the COMPACT model.    
PBPSTACK    EQU 0   ; set to 1 if compact reentrant is used.
PBPSTACKTOP EQU 0FFFFH+1; set top of stack to highest location+1.
;
;------------------------------------------------------------------------------
;   初始化PDATA区
;  Page Definition for Using the Compact Model with 64 KByte xdata RAM
;
;  The following EQU statements define the xdata page used for pdata
;  variables. The EQU PPAGE must conform with the PPAGE control used
;  in the linker invocation.
;
PPAGEENABLE EQU 1   ; set to 1 if pdata object are used.
PPAGE       EQU 0   ; define PPAGE number.
PPAGE_SFR       DATA    0A0H
START_GPNVM_CODE EQU    00H ; Start of Code
;
;------------------------------------------------------------------------------

; Standard SFR Symbols 
ACC     DATA    0E0H
B       DATA    0F0H
SP      DATA    81H
DPL     DATA    82H
DPH     DATA    83H
P2      DATA    0A0H

    NAME    ?C_STARTUP  ;定义这段汇编代码在obj文件中的名字
; 声明三个在外部定义的中断函数, 以便在本模块中调用
EXTRN CODE (TrqIsr)
EXTRN CODE (uartISR)
EXTRN CODE (FlashInterrupt)

; 声明段C_C51STARTUP和STACK存储位置
?C_C51STARTUP   SEGMENT   CODE
?STACK      SEGMENT   IDATA

; 选择STACK段,并设定STACK的size
        RSEG    ?STACK
        DS  96

; 选择地址0x00(CODE区),并跳转到0x0200(CODE区)的位置      
        CSEG    AT  0x00
        LJMP    0x0200

        EXTRN CODE (?C_START)   ;声明外部段名 C_START,以便在本模块中调用
        PUBLIC  ?C_STARTUP      ;声明在本文件中定义的段C_STARTUP为public的,以供其他模块调用

; 选择地址0x0200(CODE区),并跳转到 STARTUP1(CODE区)的位置
        CSEG    AT  0x0200      
?C_STARTUP: LJMP    STARTUP1

; 以下是中断向量表,分配每个中断的地址和对应的中断服务函数。
        CSEG AT START_GPNVM_CODE+0BH        ; IT timer 0
;       gpnvmVectorEtuCnt:
        LJMP TrqIsr
        RETI                    
;       LJMP    InterruptRoutineVectorEtuCnt

        CSEG AT START_GPNVM_CODE+13H        ; MMU COB or DOB or OVD
;       gpnvmVectorFault:
        LJMP uartISR            
        RETI

        CSEG AT START_GPNVM_CODE+23H        ; MMU COB or DOB or OVD
;       gpnvmVectorFault:
        LJMP FlashInterrupt         
        RETI

; 选择C_C51STARTUP段所在地址       
        RSEG    ?C_C51STARTUP
STARTUP1:
        MOV MMU_SEL,#01H    ; 初始化SFR: MMU_SEL
        MOV P3,#05H         ; 初始化SFR: P3

;初始化单片机的时钟频率
        MOV DPTR,#PLLADDR
        MOVX A,@DPTR
        ANL A, #0C0H
        MOV CLKSEL, A

; 初始化 IRAM (0x00 - 0xFF)
IF IDATALEN <> 0
        MOV R0,#IDATALEN - 1
;        MOV R1,#IDATASTART
        CLR A
IDATALOOP:  MOV @R0,A
;        INC R1
        DJNZ    R0,IDATALOOP
ENDIF
; 初始化 XRAM
IF XDATALEN <> 0
        MOV DPTR,#XDATASTART
        MOV R7,#LOW (XDATALEN)
  IF (LOW (XDATALEN)) <> 0
        MOV R6,#(HIGH (XDATALEN)) +1
  ELSE
        MOV R6,#HIGH (XDATALEN)
  ENDIF
        CLR A
XDATALOOP:  MOVX    @DPTR,A
        INC DPTR
        DJNZ    R7,XDATALOOP
        DJNZ    R6,XDATALOOP
ENDIF

; 初始化PDATA
IF PPAGEENABLE <> 0
        MOV P2,#PPAGE
ENDIF

IF PDATALEN <> 0
        MOV R0,#PDATASTART
        MOV R7,#LOW (PDATALEN)
        CLR A
PDATALOOP:  MOVX    @R0,A
        INC R0
        DJNZ    R7,PDATALOOP
ENDIF

; 初始化reentrant函数使用的堆栈指针(SMALL/COMPACT/LARGE)
IF IBPSTACK <> 0
EXTRN DATA (?C_IBP)

        MOV ?C_IBP,#LOW IBPSTACKTOP
ENDIF

IF XBPSTACK <> 0
EXTRN DATA (?C_XBP)

        MOV ?C_XBP,#HIGH XBPSTACKTOP
        MOV ?C_XBP+1,#LOW XBPSTACKTOP
ENDIF

IF PBPSTACK <> 0
EXTRN DATA (?C_PBP)
        MOV ?C_PBP,#LOW PBPSTACKTOP
ENDIF

        MOV SP,#?STACK-1        ;初始化堆栈指针,指向栈底

; 声明外部定义的函数B_SWITCH0,并调用之
EXTRN CODE (?B_SWITCH0)
        CALL    ?B_SWITCH0      ; init bank mechanism to code bank 0

        LJMP    ?C_START    ;调用main()函数

END
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179

这个是编译输出文件.lst中的部分代码在code区的分配情况,结合汇编代码,我们可以知道在code区某个位置存放的是什么指令。

000000H   000002H   000003H   ---    OFFS..   CODE           ?CO?STTF06?3       ;此处存放的代码为 LJMP  0x0200
000003H   00000AH   000008H   ---    ---      **GAP**
00000BH   00000EH   000004H   ---    OFFS..   CODE           ?CO?STTF06?5       ;此处存放的代码为 LJMP TrqIsr
00000FH   000012H   000004H   ---    ---      **GAP**
000013H   000016H   000004H   ---    OFFS..   CODE           ?CO?STTF06?6       ;此处存放的代码为 LJMP uartISR
000017H   000022H   00000CH   ---    ---      **GAP**
000023H   000026H   000004H   ---    OFFS..   CODE           ?CO?STTF06?7       ;此处存放的代码为 LJMP FlashInterrupt
000027H   0001FFH   0001D9H   ---    ---      **GAP**
000200H   000202H   000003H   ---    OFFS..   CODE           ?CO?STTF06?4       ;此处存放的代码为 LJMP  STARTUP1
000203H   0006E9H   0004E7H   BYTE   UNIT     CODE           ?C?LIB_CODE
0006EAH   0007A0H   0000B7H   BYTE   UNIT     CODE           ?C_C51STARTUP      ;此处存放的代码为 段?C_C51STARTUP的内容,对单片机的硬件做初始化1234567891011

下面是描述startup.A51的流程图,作为这次学习的总结。
这里写图片描述

二、STM32的完整启动流程分析

已剪辑自: https://blog.csdn.net/Setul/article/details/121685929

关于STM32的启动流程,网上有的资料在讨论几种boot模式,有的在回答启动文件的内容,在查阅了很多资料后,本文给出一个比较全面的总结和回答。

1. 根据boot引脚决定三种启动模式

在这里插入图片描述

复位后,在 SYSCLK 的第四个上升沿锁存 BOOT 引脚的值。BOOT0 为专用引脚,而 BOOT1 则与 GPIO 引脚共用。一旦完成对 BOOT1 的采样,相应 GPIO 引脚即进入空闲状态,可用于其它用途。BOOT0与BOOT1引脚的不同值指向了三种启动方式:

  1. 从主Flash启动。主Flash指的是STM32的内置Flash。选择该启动模式后,内置Flash的起始地址将被重映射到0x00000000地址,代码将在该处开始执行。一般我们使用JTAG或者SWD模式下载调试程序时,就是下载到这里面,重启后也直接从这启动。
  2. 从系统存储器启动。系统储存器指的是STM32的内置ROM,选择该启动模式后,内置ROM的起始地址将被重映射到0x00000000地址,代码在此处开始运行。ROM中有一段出厂预置的代码,这段代码起到一个桥的作用,允许外部通过UART/CAN或USB等将代码写入STM32的内置Flash中。这段代码也被称为ISP(In System Programing)代码,这种烧录代码的方式也被称为ISP烧录。关于ISP、ICP和IAP之间的区别将在后续章节中介绍。
  3. 从嵌入式SRAM中启动。显然,该方法是在STM32的内置SRAM中启动,选择该启动模式后,内置SRAM的起始地址将被重映射到0x00000000地址,代码在此处开始运行。这种模式由于烧录程序过程中不需要擦写Flash,因此速度较快,适合调试,但是掉电丢失。

总结:上面的每一种启动方式我都描述了“xxx的起始地址被重映射到了0x00000000地址,从而代码从xxx开始启动”,如下图是STM32F4xx中文参考手册中的图,可以看到类似的表述。同时,在下图中也展示了STM32F4xx中统一编址下,各内存的地址分配,注意一点,即使相应的内存被映射到了0x00000000起始的地址,通过其原来地址依然是可以访问的。
在这里插入图片描述

2. 启动后bootloader做了什么?

根据BOOT引脚确定了启动方式后,处理器进行的第二大步就是开始从0x00000000地址处开始执行代码,而该处存放的代码正是bootloader。
bootloader,也可以叫启动文件,无论性能高下,结构简繁,价格贵贱,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从main函数开始进行应用程序的设计即可。同样,STM32微控制器,无论是keiluvision4还是IAR EWARM开发环境,ST公司都提供了现成的直接可用的启动文件。
网上有很多资料分析了STM32的启动文件的内容,在此我只进行简单的表述。启动文件中首先会定义堆栈,定义中断/异常向量表,而其中只实现了复位的异常处理函数Reset_Handler,该函数内容如下(STM32F4XX,IAR编译器),可以看到其主要执行了SystemInit和__iar_program_start两个函数,其主要功能除了初始化时钟,FPU等,还会执行一个重要功能,那就是内存的搬移、初始化操作。 这是我想重点介绍的内容,同时也会回答一个疑问,就是如果从Flash启动的话,代码究竟是运行在哪儿的?在我之前接触ARM9、CortexA系列的时候,一般都是把代码搬到内部的SRAM或者外部DDR中执行的,STM32是如何呢?答案下一小节揭晓。
在这里插入图片描述

3. bootloader中对内存的搬移和初始化

本节针对程序在内置Flash中启动的情况进行分析。

在这里插入图片描述
我们知道烧录的镜像文件中包含只读代码段.text,已初始化数据段.data和未初始化的或者初始化为0的数据段.bss。代码段由于是只读的,所以是可以一直放在Flash中,CPU通过总线去读取代码执行就OK,但是.data段和.bss段由于会涉及读写为了,为了更高的读写效率是要一定搬到RAM中执行的,因此bootloader会执行很重要的一步,就是会在RAM中初始化.data和.bss段,搬移或清空相应内存区域。
因此我们知道,当启动方式选择的是从内置Flash启动的时候,代码依旧是在Flash中执行,而数据则会被拷贝到内部SRAM中,该过程是由bootloader完成的。bootloader在完成这些流程之后,就会将代码交给main函数开始执行用户代码。

  • 现在让我们思考一个问题,PC机在运行程序的时候将程序从外存(硬盘)中,调入到RAM中运行,CPU从RAM中读取程序和数据;而单片机的程序则是固化在Flash中,CPU运行时直接从Flash中读取程序,从RAM中读取数据,那么PC机能从Flash之类的存储介质中直接读代码执行吗?
  • 答案是不行。因为x86构架的CPU是基于冯.诺依曼体系的,即数据和程序存储在一起,而且PC机的RAM资源相当丰富,从几十M到几百M甚至是几个G,客观上能够承受大量的程序数据。但是单片机的构架大多是哈弗体系的,即程序和数据分开存储,而且单片的片内RAM资源是相当有限的,内部的RAM过大会带来成本的大幅度提高。

4. ISP、IAP、ICP三种烧录方式

虽然这个小节稍稍偏题,但是由于上面在3中启动方式中介绍过了ISP烧录,因此一并在此介绍剩下的两种烧录方式。
本小节摘自 https://blog.csdn.net/zhuimeng_ruili/article/details/119709888。

  1. ICP(In Circuit Programing)。在电路编程,可通过CPU的Debug Access Port 烧录代码,比如ARM Cortex的Debug Interface主要是SWD(Serial Wire Debug)或JTAG(Joint Test Action Group);
  2. ISP(In System Programing)。在系统编程,可借助MCU厂商预置的Bootloader 实现通过板载UART或USB接口烧录代码。
  3. IAP(In Applicating Programing)。在应用编程,由开发者实现Bootloader功能,比如STM32存储映射Code分区中的Flash本是存储用户应用程序的区间(上电从此处执行用户代码),开发者可以将自己实现的Bootloader存放到Flash区间,MCU上电启动先执行用户的Bootloader代码,该代码可为用户应用程序的下载、校验、增量/补丁更新、升级、恢复等提供支持,如果用户代码提供了网络访问功能,IAP 还能通过无线网络下载更新代码,实现OTA空中升级功能。
  4. IAP和ISP 的区别。
    a、ISP程序一般是芯片厂家提供的。IAP一般是用户自己编写的
    b、ISP一般支持的烧录方式有限,只有串口等。IAP就比较灵活,可以灵活的使用各种通信协议烧录
    c、isp一般需要芯片进行一些硬件上的操作才行,IAP全部工作由程序完成,不需要去现场
    d、isp一般只需要按格式将升级文件通过串口发送就可以。IAP的话控制相对麻烦,如果是OTA的话还需要编写后台的。
    e、注意,这里介绍的bootloader功能显然跟之前介绍的启动文件bootloader有所区别,其目的是为了能接受外部镜像进行烧录,而不是为了运行普通用户程序。

5. 参考资料

  1. STM32 BOOT模式配置以及作用: https://www.cnblogs.com/huanzxj/p/6273014.html
  2. 高手带你分析STM32 的启动过程: https://blog.csdn.net/zhuimeng_ruili/article/details/108789033
  3. [原创]在main()之前,IAR都做了啥?https://www.cnblogs.com/mssql/archive/2011/01/29/tt146.html
  4. STM32启动过程启动文件分析:https://www.sohu.com/a/77623300_119709
  5. STM32中的程序在RAM还是FLASH里运行?https://blog.csdn.net/u012252959/article/details/80800559

从上电到启动,一文读懂STM32启动全流程

已剪辑自: https://blog.csdn.net/wangbotao1990/article/details/108634587?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-108634587-blog-121685929.pc_relevant_3mothn_strategy_and_data_recovery&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-108634587-blog-121685929.pc_relevant_3mothn_strategy_and_data_recovery&utm_relevant_index=5

  • Boot1 Boot0

  • bootloader

  • 中断向量表

  • 启动流程

    很多同学会使用STM32进行应用/驱动开发,一般开发就是从main()函数开始,绝大多数人没有关注或并不关注类似STM32的MCU是怎么从上电运行到mian()函数的,今天我就针对这部分的内容进行了一个大整合,帮助大家理解这个过程。

1、直接上电?不,上电前还暗藏玄机

也许你会说可不就是直接给电就行吗?我啥也没配置,就轻松进入main()函数了。
其实,在上电前有两个引脚对MCU的启动有些关键的影响:BOOT0和BOOT1,这两个引脚的电平状态直接影响了芯片在复位上电后从芯片的哪个位置开始执行代码。

Boot1=x Boot0=0 从内置闪存flash开始启动,这是正常的启动模式。
通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;

STM32的内存flsh,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。

Boot1=0 Boot0=1 从系统存储器启动
通过boot引脚设置可以将中断向量表定位于内置Bootloader区
这是芯片一块特殊的区域,由ST出厂烧录预置的一段BootLoader,也就是我们常说的 ISP(In System Programing,在系统中编程 )程序;这是一块ROM,一旦出厂烧录即无法修改,只读,使用较少。
一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。
从System Memory启动就可以使用串口来下载我们的程序,因为在芯片出厂前ST官方已经把一段自举程序(BootLoader程序)固化到这一块存储区。其通过串口来接收数据并烧写到用户闪存存储器的起始地址(0x08000000)。只能烧写到这个地址,若keil里设置的地址不是这个地址,则编译出来的文件将烧录不成功。
用户闪存,即User Flash,同时也称为Main Flash。

注意:不同系列不同型号的STM32固化的BootLoader是不同的,即使用的通讯接口是不同的。

**IAP程序与ISP程序有什么不同?**
https://www.cnblogs.com/zhengnian/p/11538465.html

ISP程序、IAP程序、应用程序的关系示意图如下:
1234

在这里插入图片描述

如果我们的产品中的程序只有应用程序,则此时0x08000000地址存放的程序就是应用程序。ISP程序、应用程序的关系示意图如下:
在这里插入图片描述

Boot1=1 Boot0=1 从SRAM启动
通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处;

因为是使用SRAM启动,自然没有程序存储的能力,一般用于代码调试的时候,减少了调试阶段还要反复全擦除flsh的操作。

2、Bootloader

启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从main函数开始进行应用程序的设计即可。

Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。

无论是哪种启动方式,上电之后,都会有一段BootLoader引导程序,如果是从内置flash启动,则直接从0x08000000地址开始读取;如果是从系统存储器启动,则从ISP引导程序开始,该地址位置根据不同芯片地址也不同,可以查看芯片手册找到起始地址。然后引导跳转到0x08000000,IAP程序+应用程序,若无IAP程序,则0x08000000存放的就是应用程序。
IAP程序即是我们常常关注的用户BootLoader程序,可以根据实际需求修改。

  • 上电启动后,首先找到
  • 单片机首先进入复位中断 Reset_Handler,即汇编文件的复位中断处理函数。
Bootloader中的关键部分为:
1、划分bootloader和app的空间
2、接收编译好的app的bin文件,写入flash
3、实现跳转

App里面主要修改的地方是
1、ROM起始地址和分配的空间大小
2、中断向量表重定向
3、生成bin文件
123456789

三、VxWorks的启动过程

已剪辑自: https://www.vxworks.net/bsp/88-vxworks-boot-process

摘要: 镜像种类不同,VxWorks的启动过程会有所不同。 我们项目中使用的是加载型VxWorks镜像

VxWorks Boot Process

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8i9N1EVn-1664767544921)(C:\Users\10521\AppData\Roaming\Typora\typora-user-images\image-20221003090219500.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5R3iFU8Z-1664767544922)(C:\Users\10521\AppData\Roaming\Typora\typora-user-images\image-20221003090239491.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-je5zbWWJ-1664767544922)(C:\Users\10521\AppData\Roaming\Typora\typora-user-images\image-20221003090252827.png)]


四、操作系统(RTOS)的启动流程是怎样的?

已剪辑自: https://www.toutiao.com/article/6830717264627499523/?&source=m_redirect

一般说到实时系统(RTOS)时,我们都会想到嵌入式领域的应用。由于嵌入式板卡硬件和资源的限制,往往我们操作系统的映像文件都会存储在Flash之中,上电启动时,一般会依靠一个启动软件,比如常见的Linux下的Uboot或者VxWorks下的Bootrom软件。这些Boot软件会初始化硬件资源如:CPU的时钟、DDR和Cache等关键参数,然后将Flash中的系统内核文件引导至DDR之中,进而执行系统的核心功能。

Boot软件的一种

本文以VxWorks为例说明一下RTOS是如何启动的,启动过程中都做了哪些工作?在进入系统启动工作前,先来了解一下VxWorks常用的Bootrom类型

Boot软件

1、用于可加载VxWorks映象的BOOTROM

用于可加载VxWorks映象的BOOTROM包含两部分:起始引导程序(BootStrap Programs)和ROM引导程序(ROM Boot Programs)

起始引导程序驻留在ROM中,主要包含:

汇编级的硬件初始化程序romInit.s,用于系统的基本初始化,设置一些重要寄存器的初始值,进行存储器的映射搬移程序bootInit.c,将ROM引导程序拷贝至RAM的高端地址RAM_HIGH_ADRS,然后跳转到此处执行ROM引导程序。

ROM引导程序起初存放在ROM中,初始化时被拷贝到RAM中,主要用于系统的进一步初始化,并配置加载方式,将VxWorks映象加载至RAM。可分为三种不同的类型:

1)压缩的ROM引导程序,在拷贝的过程中需要解压缩,在RAM中执行;

2)未压缩的ROM引导程序,可直接拷贝,在RAM中执行;

3)驻留ROM的ROM引导程序,仅拷贝ROM引导程序的数据段,代码段仍旧在ROM中执行。

2、用于基于ROM的VxWorks映象的BOOTROM

用于该映象的BOOTROM包含两部分:

起始引导程序(BootStrap Programs)和基于ROM的VxWorks映象。搬移程序bootInit.c负责将VxWorks映象的文本段和数据段搬移到用户定义的低端内存地址RAM_LOW_ADRS,如果需要进行必要的解压缩,然后直接启动VxWorks映像。

因此BOOTROM的容量相对于可加载VxWorks映象的BOOTROM要大一些,但无需在主机目录下维护一个可用的VxWorks映象。

基于ROM的VxWorks BOOTROM有压缩和未压缩之分。

3、用于驻留ROM的VxWorks映象的BOOTROM

用该映象的BOOTROM包含两部分:

起始引导程序(BootStrap Programs)和驻留ROM的VxWorks映象,VxWorks系统文本段驻留在ROM,搬移程序bootInit.c负责将数据段和bss段搬移到用户定义的低端内存地址RAM_LOW_ADRS,直接启动VxWorks映像(含符号表)。此时,RAM_LOW_ADRS是VxWorks映象的加载点,它也是VxWorks数据段的起始点。

VxWorks的启动过程

根据上述所采用的BOOTROM的不同,VxWorks的启动过程会有所不同,下面主要介绍一下使用可加载VxWorks映像的启动过程

操作系统

1、目标板加电运行(汇编语言)

目标板加电之后,程序指针指向RESET中断程序入口处,开始执行初始化程序romInit.s,设置机器状态字及其它硬件相关寄存器,关闭中断,禁止程序和数据CACHE,初始化内存,并设置堆栈指针,保存启动类型,调用romStart.c中的romStart( )

2、开始运行第一个C程序

第一个C程序romStart.c的函数romStart( )入口地址,根据堆栈中的参数决定是否清零内存RAM(如是冷启动cold start,则清零),根据不同的bootRom文件,把ROM中数据段和文本段拷贝到RAM(如果ROM代码是压缩的,还要解压)。

C语言程序

3、运行RAM中的程序

程序跳到RAM入口地址(usrConfig.c中的函数usrInit( ) ),usrInit( )中清零bss段(这也是未赋初始值的全局变量在编译后初始值为0的原因),调用excVecInit() 安装异常向量(excVecInit会将excIntHandle注册到相应的异常上),初始化异常处理程序,调用cacheLibInit(),设置cache的指令与数据工作模式,调用sysHwInit( )对板级硬件初始化,调用usrKernelInit( )配置wind Kernel, 调用KernelInit( ) 进行内核初始化。

4、创建第一个根任务

初始化内核及内存池, 主要是中断堆栈及根任务堆栈初始化,初始化任务Tcb 并生成根任务usrRoot( )。kernelInit( ) 调用intLockLevelSet( ),关闭循环模式,创建一个中断堆栈(如果结构支持的话)。然后从内存池的顶部创建一个根堆栈和TCB,创建一个根任务usrRoot,并终止usrInit( )线程的执行。此时使能中断,所有的中断源已被关闭,未决中断已被清除。

本系统的根任务函数usrRoot( )在prjConfig.c中。在该任务中初始化内存,系统时钟,I/O系统,标准输入输出错,异常处理,外围设备等。BPC初始任务usrRoot具体所处理的内容如下:

void usrRoot (char *pMemPoolStart, unsigned memPoolSize)
{
excIntNestLogInit();                    
vxMsrSet(vxMsrGet() | taskMsrDefault); 
usrKernelCoreInit ();                              
memInit (pMemPoolStart, memPoolSize);   
memPartLibInit (pMemPoolStart, memPoolSize);   
usrMmuInit ();                    
sysClkInit ();                      
selectInit (NUM_FILES);         
usrIosCoreInit ();                    
usrKernelExtraInit ();             
usrIosExtraInit ();                 
usrNetworkInit ();              
selTaskDeleteHookAdd ();     
usrToolsInit ();                         
cplusCtorsLink ();  
usrAppInit ();          
}

最后会调用usrAppInit.c中的usrAppInit ( )进行用户级应用模块的初始化。

usrAppInit( )最后会调用BootApp.c中的**BootAppStart( )**进入autoboot或bootLoad, 根据单板设计选择不同方式加载VxWorks映像文件,如通过串口、网口、硬盘等方式加载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M03UPZPo-1664767544922)(https://p3-sign.toutiaoimg.com/pgc-image/fa609643cc61482a99d265dc68e5b9b9~noop.image?_iz=58558&from=article.pc_detail&x-expires=1665363786&x-signature=z4GjAY9zxxqBgx9d%2BREfU0RjodE%3D)]

VxWorks系统启动流程

5、系统开始启动运行

启动VxWorks系统的第一步就是将系统映象加载到主内存, 在开发初期, 这通常是在VxWorks boot Rom 的控制下,从开发主机上下载, 我们正式运行的系统中。VxWorks映象被加载到ram后, boot Rom会复位系统并将控制权交给VxWorks的起始进入点sysInit()。 在makefile和 config.h文件里,已将这个进入点设置成位于地址RAM_LOW_ADRS。

函数sysInit()位于系统特定的汇编语言模块sysALib.s中。它可以锁住中断,关闭cache(如果使用了话),初始化处理器的寄存器(包括C堆栈指针)至缺省值。它还会关闭跟踪,清除所有未决的中断,并调用一个位于usrConfig.c 模块的C语言子程序:usrInit()。对于某些目标板,sysInit()还执行一些必要的与系统有关的硬件初始化,以便在usrInit()中执行完剩余的初始化内容。仅供usrInit()使用的初始堆栈指针,被设置成位于系统映象(RAM_LOW_ADRS)以下, 向量表以上的位置。

函数usrInit()(位于usrConfig.c中),储存有关引导类型的信息,处理在内核启动之前必须执行的初始化,而后启动内核执行。它是运行于VxWorks内的第一个C函数。此时,所有的中断都已被锁住。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GsHNBOHr-1664767544922)(https://p3-sign.toutiaoimg.com/dfic-imagehandler/cfd7ee2e-893e-4aaa-a03d-de9b771f3a0e~noop.image?_iz=58558&from=article.pc_detail&x-expires=1665363786&x-signature=%2FitpwbOmwsKCT7JrC%2B1bwQOEewo%3D)]

多任务

6、启动第一个任务

当多任务内核启动执行以后,所有的VxWorks多任务工具就可以用了。控制权被传送至**usrRoot( )**任务,并完成初始化系统。usrRoot( ) 执行以下操作:

  1. 初始化系统时钟

  2. 初始化I/O系统和驱动

  3. 创建控制台设备

  4. 设置标准输入和标准输出

  5. 安装异常处理和登陆

  6. 初始化管道驱动器

  7. 初始化标准I/O

  8. 创建文件系统设备并安装磁盘驱动器

  9. 初始化浮点支持

  10. 初始化性能监视工具

  11. 初始化网络

  12. 初始化可选的工具

  13. 初始化WindView

  14. 初始化目标代理

  15. 执行一个用户提供的启动脚本

  16. 初始化VxWorks Shell

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DcykuiaI-1664767544923)(https://p3-sign.toutiaoimg.com/dfic-imagehandler/7507a567-3156-4733-8174-4c684d43d7a9~noop.image?_iz=58558&from=article.pc_detail&x-expires=1665363786&x-signature=IDyUE2gyGGMaqKsFY35WBfvK5qE%3D)]

第一个任务

总结

本文以VxWorks为例说明了实时系统(RTOS)从上电启动到内核开始执行shell的整个过程,对于大多数的RTOS来说,启动过程都大同小异,同样的方法可以在别的RTOS中见到,整个过程基本分为汇编语言Flash运行阶段、C语言运行阶段、单任务阶段和多任务阶段。


五、Linux 系统启动流程

已剪辑自: https://jaminzhang.github.io/linux/Linux-boot-process/

引言

最近在看 Cobbler 相关资料,提到了要熟悉 Linux 系统的启动流程,很久没有接触这方面了,有些生疏遗忘了,
于是重新复习了下,真要深究,可以分为好多步。

首先贴一张总结得比较好图:

Linux 操作系统启动流程图

下面进行文字版本再说明:

第一阶段:硬件引导启动阶段

1.1 POST(Power On Self Test) 加电自检
1.2 BIOS
	1.2.1 初始化硬件
	1.2.2 查找启动介质
		HDD: 查找启动硬盘的第一个扇区(MBR/BootSector)
1.3 MBR
	1.3.1 Bootloader(启动装载程序)
		GRUB
		分区表

第二阶段:BootLoader 启动引导阶段

2.1 Stage1
	执行 BootLoader 主程序(位于 MBR 前 446个字节),它的作用是启动 Stage1.5 或 Stage2
2.2 Stage1.5
	Stage1.5 是桥梁,由于 Stage2 较大,存放在文件系统中,需要 Stage1.5 引导位于文件系统中的 Stage2
2.3 Stage2
	Stage2 是 GRUB 的核心映像
2.4 grub.conf
	Stage2 解析 grub.conf 配置文件,加载内核到内存中

第三阶段:内核引导阶段

3.1 /boot/kernel and Kernel parameter 
	内核初始化,加载基本的硬件驱动
	
3.2 /boot/initrd
	引导 initrd 解压载入
	3.2.1 阶段一:在内存中释放供 kernel 使用的 root filesystem
				执行 initrd 文件系统中的 init,完成加载其他驱动模块
	3.2.2 阶段二:执行真正的根文件系统中的 /sbin/init 进程

第四阶段:Sys V init 初始化阶段

4.1 /sbin/init
	4.1.1 /etc/inittab
		init 进程读取 /etc/inittab 文件,确定系统启动的运行级别
	4.1.2 /etc/rc.d/rc.sysinit
		执行系统初始化脚本,对系统进行基本的配置
	4.1.3 /etc/rc.d/rcN.d
		根据先前确定的运行级别启动对应运行级别中的服务
	4.1.4 /etc/rc.d/rc.local
		执行用户自定义的开机启动程序
4.2 登录
	4.2.1 /sbin/mingetty (命令行登录)
		验证通过 执行 /etc/login 
		加载 /etc/profile  ~/.bash_profile  ~/.bash_login  ~/.profile
		取得 non-login Shell
		
	4.2.2 /etc/X11/prefdm (图形界面登录)
		gdm kdm xdm
		Xinit
		加载 ~/.xinitrc  ~/.xserverrc

第五阶段:启动完成

Linux 的启动流程分析
Linux 的启动流程
Linux 操作系统启动过程详解
详解 Linux 系统的启动过程及系统初始化
Linux 系统启动过程


六、STM32单片机是如何软硬件结合的

转载于:https://mp.weixin.qq.com/s/ADtZRR7Fewb2jxOOdbKlpg


七、单片机驱动开发

已剪辑自: https://zhuanlan.zhihu.com/p/34283108

首发平台:微信公众号baiwenkeji

这是arm裸机1期加强版第1课第2节课程的wiki文字版。

为什么没前途也要学习单片机?

因为它是个很好的入口。

学习单片机可以让我们抛开复杂的软件结构,先掌握硬件操作,如:看原理图、芯片手册、写程序操作寄存器等。 在上一节视频里,我刚把单片机贬得一无是处,说单片机没前途了,这节视频,我又要告诉你们,没有前途,也要学习单片机。为什么?

首先,我说不用学习单片机,是指不要使用老一套得学习方法学习单片机。什么叫老一套的方法?

· 硬件上:不要使用C51、STM32这些专用的单片机开发板。如果以后,你不打算从事单片机开发,你用这些芯片干嘛,研究了两三个月,把这些寄存器都用清楚了,你又用不上,没必要啊。

· 软件上:不要使用Keil、MDK等集成度太高的软件。你用这些软件,你写个main()就可以了,然后调用各种库,进行傻瓜式操作。这些好用的工具,封装了很多技术细节,使得我们没法了解裸机、单片机的本质。

以后我们会使用新一套的方法来进行单片机的开发。新一套的方法,我们后面再介绍。

img

我们之所以还要学习单片机,是因为它里面的知识,对我们后续学习Linux还是有用的。我们首先来看看,一个Linux系统是怎么一回事。 一个嵌入式Linux系统的软件组成: 单片机大全Bootloader–>Linux驱动–>Linux APP–>Linux GUI(Android/QT) 。我们PC机一上电的时候,黑色屏幕上会显示BIOS,这个BIOS目的是去启动Windows内核。Windows内核再挂载C盘(系统盘)、D盘(应用盘),最后再去启动应用程序,像QQ、网游等。 同样的道理,我们的Android手机或者工控设备,也有BIOS,但嵌入式Linux系统里面不叫BIOS,叫Bootloader,他的目的是去启动Linux内核。 它首先也是识别应用程序所在的存储设备,挂载根文件系统(在Windows系统里面的C盘、D盘,在Linux里面称为根文件系统)。最后去启动应用程序。

img

仔细的分析下Bootloader,它去启动内核,它去哪里启动内核呢? 显然是去某个地方读出内核,就比如说BIOS是去C盘上读出Windows内核,我们的Bootloader是去Flash或者SD卡读取内核。 因此Bootloader要拥有读取Flash或者SD卡的能力。有些Bootloader还要显示logo,因此还要具有操作LCD的能力。Bootloader还要设置开发板的环境,比如,初始化时钟、初始化内存、还要设置网卡等。 这么多事情,都是在Bootloader里面实现的,太复杂了,如果你一来就分析整个Bootloader是非常困难的。

那我们怎么学习呢? 把他拆开,写出单独的程序,比如:LED点灯、时钟、网卡、Flash都单独写个程序来练习,这些不就是单片机程序吗?所以说,Bootloader是单片机程序的大全。我们为了更好的学习Bootloader,我们应该事先一个一个练习硬件,当我们熟悉每个硬件后,再组合起来,就是一个Bootloader。

img

我们再来看看Bootloader启动内核之后,内核再去挂载根文件系统,意味着内核也要有操作硬件的能力,这就是驱动程序。我们首先来看看一个简单的驱动程序是什么样子。 首先我们的应用程序是调用open()、read()、write()这些标准的接口去访问硬件。那么就进入驱动程序里面,驱动程序里面有对应的drive_open()、drive_read()、drive_write()。最后在驱动程序里面,去配置硬件。 比如一个LED点灯驱动,那么drive_open()要把GPIO设置为输出引脚,drive_read(),返回GPIO状态,driver_write()则写GPIO,让引脚输出高电平或者低电平。

img

对于我们的LED驱动程序,你需要提供drive_open()、drive_read()、drive_write()这些接口,这就是它的框架。具体的怎么操作硬件,就是硬件操作。 所以说,我们事先在单片机里面,熟悉熟练的掌握硬件操作。即驱动程序的组成:

驱动程序=软件框架+硬件操作

你需要学会看原理图、看硬件怎么连接、看芯片手册、知道怎么读写寄存器。这一切都可以先在单片机里面学习,去掌握。以后学习Linux驱动时,把重点放在软件框架就行了。

我们可以事先学习单片机,单片机的学习可以让我们先抛开复杂的软件结构,先掌握硬件的操作,如:看原理图、芯片手册、写程序操作寄存器等。 这就是为什么单片机没有前途,我们也要学习。是因为他里面涉及的硬件操作,对我们后续的学习,非常有用处。

现在我们知道了,我们学习单片机,不是为了掌握单片机的开发技能,而是为了掌握Bootloader,掌握硬件操作

img


八、从单片机步入Linux驱动开发(概念和Demo)

已剪辑自: https://blog.csdn.net/qq_41960161/article/details/121588723?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%8D%95%E7%89%87%E6%9C%BA%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-121588723.nonecase&spm=1018.2226.3001.4187

单片机的裸跑能实现很多功能,代码量也不大,但是在嵌入式领域不学习Linux,那写再多的代码也枉然,因为有无数的大牛给Linux添砖加瓦,并且Linux有开放源码,易于移植,资源丰富,免费等优点。

首先我们看一个Linux系统的内容:可以分为应用程序、库、操作系统、驱动程序。一共五个层次,如下图:
在这里插入图片描述

应用程序层是直接使用open read write ioctl这些库函数。Linux驱动开始是要根据项目需求编写具体的驱动程序,也就是open read write ioctl这些函数的具体内容。不同的外设有不同的open read…内容。

Linux驱动程序的分类:
可以分为3类。1、字符设备(character device) 2、块设备(block device) 3、网络接口(network interface)。

字符设备是能够像字节流一样被访问的设备,就是说对它的读写是以字节为单位的。比如串口,和简单的点灯驱动程序。

块设备上的数据以块的形式存放,比如NAND Flash上的数据就是以页为单位存放的。

网络接口同时具有字符设备,块设备的部分特点。网络设备有小到几个字节的,也有大到几千个字节的。UNIX式的操作系统访问网络接口的方法是给他们分配一个唯一的名字(比如eht0),但这个名字在文件系统中不存在对应的节点项。应用程序,内核和网络驱动程序间的通信完全不同于字符设备,块设备,库、内核提供了一套和数据包传输相关的函数,而不是open,read等函数。

驱动程序的加载和卸载:
可以将驱动程序编译进内核中,也可以将它作为模块在使用时再加载。在配置内核时,如果某个配置项被设为m,就表示它将会被编译成一个模块。

当使用insmod加载模块时,模块的初始化函数被调用,它用来向内核注册驱动程序;当使用rmmod卸载模块时,模块的清除函数被调用。在驱动代码中,这两个函数要么取固定的名字:init_module 和 cleanup_module,要么使用以下两行来标记它们

module_init(demo_init);
module_exit(demo_cleanup);
12

记住,这是固定格式。

Linux操作系统将所有的设备都看成文件,以操作文件的方式访问设备。应用程序不能直接操作硬件,而是使用统一的接口函数调用硬件驱动程序。这组接口被称为系统调用。

对于字符设备,open read write这些函数在集合在file_operation类型的数据结构中。file_operation结构在Linux内核的include/linux/fs.h文件中定义,整个结构体如下:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*dir_notify)(struct file *filp, unsigned long arg);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
};

当应用程序使用open函数打开某个设备时,上面结构体中的open成员就会调用。所以从这个角度来说,编写字符设备的驱动程序就是为具体硬件的file_operation结构编写具体的函数内容。不用全部函数实现,需要什么函数就编写什么函数。

每个设备都有一个file_operation,都用用open函数来调用这些设备,那他们之间有什么区别呢。有两个区分要点:
1、设备文件有主/次设备号。在PC上运行命令ls /dev/ttyS0 /dev/hda1 -l可以看到以下信息:

brw-rw----    1   root    disk   3,  1, Jan  30  2003  /dev/hda1
crw-rw----    1   root    uucp   4,  64, Jan 30  2003  /dev/ttyS0

brw-rw----中的b表示/dev/had1是个块设备,它的主设备号为3,次设备号为1;
crw-rw----中的c表示/dev/ttyS0是个字符设备,它的主设备号为4,次设备号为64.

2、模块初始化时,将主设备号与file_operation结构一起向内核注册。在初始化一个设备时,就会向内核注册,用register_chrdev函数注册。

所以编写一个(简单的)字符驱动程序的过程就可以分为两步:
1、编写驱动程序初始化函数。
2、构造file_operation结构中要用到的各个成员函数。

下面开始分析一个LED驱动程序Demo 。Demo是运行在s3c24c10芯片上的。
首先需要分析原理图,我们假设:
要点亮一个LED,引脚输出0 。
要熄灭一个LED,引脚输出1 。
模块的初始化函数和卸载函数如下:

static int __init s3c24xx_leds_init(void)
{
    int ret;

    /* 注册字符设备驱动程序
     * 参数为主设备号、设备名字、file_operations结构;
     * 这样,主设备号就和具体的file_operations结构联系起来了,
     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
     * LED_MAJOR可以设为0,表示由内核自动分配主设备号
     */
    ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
    if (ret < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return ret;
    }
    
    printk(DEVICE_NAME " initialized\n");
    return 0;
}

/*
 * 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数 
 */
static void __exit s3c24xx_leds_exit(void)
{
    /* 卸载驱动程序 */
    unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}

/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c24xx_leds_init);
module_exit(s3c24xx_leds_exit);

注意看最后两个函数:module_init和module_exit这里就将具体的设备对应起来了。执行“insmod s3c24xx_leds.ko”命令时就会调用s3c24xx_leds_init函数,这个函数的核心就是register_chrdev函数,将主设备号LED_MAJOR与file_operations结构s3c24xx_leds_fops联系起来。执行rmmod s3c24xx_leds.ko命令时就会调用s3c24xx_leds_exit函数,它进而调用unregister_chrdev函数卸载驱动程序,功能与register_chrdev函数相反。

下面看看s3c24xx_leds_fops的组成

/* 这个结构是字符设备驱动程序的核心
 * 当应用程序操作设备文件时所调用的open、read、write等函数,
 * 最终会调用这个结构中指定的对应函数
 */
static struct file_operations s3c24xx_leds_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   s3c24xx_leds_open,     
    .ioctl  =   s3c24xx_leds_ioctl,
};

file_operations 类型的s3c24xx_leds_fops 结构是驱动中最重要的数据结构,编写字符设备驱动程序的主要工作也是填充其中的各个成员。比如在字符设备里的open ioctl成员被设为s3c24xx_leds_open s3c24xx_leds_ioctl函数。

s3c24xx_leds_open 的代码如下:

/* 应用程序对设备文件/dev/leds执行open(...)时,
 * 就会调用s3c24xx_leds_open函数
 */
static int s3c24xx_leds_open(struct inode *inode, struct file *file)
{
    int i;
    
    for (i = 0; i < 4; i++) {
        // 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能
        s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);
    }
    return 0;
}

在应用程序执行open(…)系统调用时,s3c24xx_leds_open将被调用。用来使能GPIO为输出模式。s3c2410_gpio_cfgpin是用来配置GPIO引脚模式的,与单片机类似。

s3c24xx_leds_ioctl函数的代码如下:

/* 应用程序对设备文件/dev/leds执行ioclt(...)时,
 * 就会调用s3c24xx_leds_ioctl函数
 */
static int s3c24xx_leds_ioctl(
    struct inode *inode, 
    struct file *file, 
    unsigned int cmd, 
    unsigned long arg)
{
    if (arg > 4) {
        return -EINVAL;
    }
    
    switch(cmd) {
    case IOCTL_LED_ON:
        // 设置指定引脚的输出电平为0
        s3c2410_gpio_setpin(led_table[arg], 0);
        return 0;

    case IOCTL_LED_OFF:
        // 设置指定引脚的输出电平为1
        s3c2410_gpio_setpin(led_table[arg], 1);
        return 0;

    default:
        return -EINVAL;
    }
}

应用程序执行系统调用ioclt时,s3c24xx_leds_ioctl函数将被调用。

驱动程序编译:
将写好的驱动程序s3c24xx_leds.c文件放入Linux内核drivers/char子目录下,在drivers/char/Makefile中增加一行
obj-m += s3c24xx_leds.o//将驱动程序加入编译的队列
然后在内核根目录下执行"make modules",就可以生成模块drivers/char/s3c24xx_leds.ko。然后将该.ko文件放入你的嵌入式板子根文件系统的/lib/modules/2.6.22.6/目录下,就可以使用“insmod s3c24xx_leds”、“rmmod s3c24xx_leds”命令进行加载、卸载了。

最后一步
驱动程序测试:
首先需要在你的电脑里编写一个测试主程序:led_test.c。如下:

int main(int argc, char **argv)
{
    unsigned int led_no;
    int fd = -1;
    
    if (argc != 3)
        goto err;
        
    fd = open("/dev/leds", 0);  // 打开设备
    if (fd < 0) {
        printf("Can't open /dev/leds\n");
        return -1;
    }
    
    led_no = strtoul(argv[1], 0, 0) - 1;    // 操作哪个LED?
    if (led_no > 3)
        goto err;
    
    if (!strcmp(argv[2], "on")) {
        ioctl(fd, IOCTL_LED_ON, led_no);    // 点亮它
    } else if (!strcmp(argv[2], "off")) {
        ioctl(fd, IOCTL_LED_OFF, led_no);   // 熄灭它
    } else {
        goto err;
    }
    
    close(fd);
    return 0;
    
err:
    if (fd > 0) 
        close(fd);
    usage(argv[0]);
    return -1;
}

其中open.ioctl最终会调用驱动程序中的s3c24xx_leds_open、s3c24xx_leds_ioctl函数。
还需要在你的电脑里编写Makefile文件,如下:

CROSS=arm-linux-

all: led_test

led_test: led_test.c
	$(CROSS)gcc -o $@ led_test.c -static

clean:
	@rm -rf led_test *.o

然后自己电脑里执行make命令生成可执行程序led_test,将这个可执行文件放入板子根系统/usr/bin/目录。

然后,在板子跟文件系统建立如下设备文件:

mknod /dev/leds c 231 0//231是主设备编号,0是次设备编号。

接下来就可以直接运行les_test命令来操作LED了,点亮和熄灭如下:

led_test 1 on
led_test 1 off

到目前为止,一个简单LED驱动程序就编写好了,有问题可以交流一下,以上的内容学习自韦山东的书籍~

九、从VxWorks的角度来看控制硬件

这个可以参考我总结的一篇文章:

十、实际项目中,怎么将开发板和各种硬件交联起来

在实际的开发过程中,我们会根据不同的需求,选择不同的开发板,选择不同的配件。

我们如果想让开发板和这些配件都正常交联通信起来,我们是需要进行哪些设置?需要哪些固件或者驱动?

之前的一篇文章讲解了固件和驱动的区别:固件与驱动的区别

有无操作系统的区别:嵌入式设备系统有无操作系统的区别

通过上面这些分析,我们会产生以下疑问:

  1. 在单片机中,设备的驱动是否和开发板有关?对于同一个设备,针对不同的开发板,需要有不同的驱动?
  2. 在单片机中,设备的驱动是否和操作系统有关?即对于同一块开发板,是否用裸机和操作系统是一样的驱动?

第一个问题的话,需要在后续的开发板学习过程中去了解。

第二个问题,我的看法是,裸机和操作系统可以是一样的驱动,就算有操作系统,只要和在裸机中一样调用驱动,结果是一样的。当然有操作系统可以将驱动融入该操作系统的设备驱动框架中,这个时候就不太一样了。

十一、总结

通过本文,理清了从单片机上电到加载操作系统(如果有的话),再进入用户应用的过程;也理清了硬件、驱动、应用直接的关系;理清了应用是怎么通过驱动来控制硬件的。

通过这篇文章,希望读者能够建立一种对单片机启动、运行的整理框架,在日常沉浸于细节琐碎的工作时,也能够时常站在一个宏观整体的视角来看看单片机的启动、运行过程。

相关文章:

  • 猿创征文 | 什么是PHP,PHP如何创建数据库
  • Kubernetes — StatefulSet 管理与使用
  • 想学习网络安全一定要学习web
  • 【leetcode刷题】数组篇
  • 基于VUE+Echarts大屏数据展示150套 (集合)
  • 【深度学习100例】—— 基于pytorch使用LSTM进行文本情感分析 | 第7例
  • 【基础巩固】详细总结对数组的理解
  • ⌈Linux_ 感受系统美学⌋ 剖释“Linux下一切皆文件” ,底层级操作增进Linux内功
  • 哪些是模糊用语-《软件方法》自测题解析020
  • 【设计模式】-创建型模式-第2章第5讲-【对象池模式】
  • 125款浪漫七夕表白网站源码【建议收藏】HTML+CSS+JavaScript
  • 基于JAVA忻府区饭中有豆粮油销售系统计算机毕业设计源码+系统+数据库+lw文档+部署
  • 毕业设计 基于单片机的风速测量系统 - 物联网 嵌入式 stm32 arduino
  • 【MSP430G2553】图形化开发笔记(4) Timer_A 定时器
  • 【老板要我啥都会】|前端升全栈之项目使用express重构项目(上篇)
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • Java编程基础24——递归练习
  • Ruby 2.x 源代码分析:扩展 概述
  • Spring Cloud Feign的两种使用姿势
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • Vue2 SSR 的优化之旅
  • webpack入门学习手记(二)
  • 闭包--闭包之tab栏切换(四)
  • 成为一名优秀的Developer的书单
  • 后端_MYSQL
  • 缓存与缓冲
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 看完九篇字体系列的文章,你还觉得我是在说字体?
  • 力扣(LeetCode)56
  • 码农张的Bug人生 - 见面之礼
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • Semaphore
  • ​Java并发新构件之Exchanger
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • #微信小程序:微信小程序常见的配置传旨
  • (02)vite环境变量配置
  • (C++17) optional的使用
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (Python第六天)文件处理
  • (SpringBoot)第七章:SpringBoot日志文件
  • (分布式缓存)Redis持久化
  • (理论篇)httpmoudle和httphandler一览
  • (六)软件测试分工
  • (未解决)macOS matplotlib 中文是方框
  • (转)iOS字体
  • (转)Sql Server 保留几位小数的两种做法
  • (转)原始图像数据和PDF中的图像数据
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • .[backups@airmail.cc].faust勒索病毒的最新威胁:如何恢复您的数据?
  • .Mobi域名介绍
  • .NET Entity FrameWork 总结 ,在项目中用处个人感觉不大。适合初级用用,不涉及到与数据库通信。
  • .net分布式压力测试工具(Beetle.DT)
  • .net与java建立WebService再互相调用