本篇文章整理下嵌入式开发中一些入门的基础技能,都是根据以往的工程经验整理,适用于之前没做过嵌入式开发的新手。

嵌入式开发流程一般如下,一般是在PC机的Windows系统下安装Ubuntu虚拟机,搭建嵌入式开发环境及交叉编译环境,开发完成后,编译得到bin文件,然后在Windows下将bin文件通过串口或网络下发到嵌入式板卡,程序在嵌入式板卡上运行。

1. RS232串口接孔图解
串口,一般也指COM接口,传输速度较慢,适合远距离传输。这里用于主机与嵌入式版之间的数据通信,一般用于指令下发等。按照协议标准还分为RS-232-C、RS-422、RS485等。其中RS-232也是最常用的串口,称为标准串口。下面仅介绍RS-232的的接线图。RS232串口为9针接口,分为公头与母头,PC机上的串口一般为公头,如下图。

 各个针孔功能如下:

最简单的串行接口需要的信号线是2数据发送(TxD)、3数据接收(RxD)和5信号地(GnD) ,只需要接通这3根线即可实现上位机PC与嵌入式板的通信,如下图:

 2. 上位机与嵌入式板串口通信工具
一般使用SecureCRT工具进行上位机与板卡通信。

(1)串口连接
打开快速连接

协议选择Serial,端口与波特率根据实际情况而设置,

点击连接,弹出新窗口,敲回车键,出现以下界面表示上位机与板卡连接成功。

(2)使用Telnet连接
Telnet连接就是使用网络(即网线)实现PC机与板卡通信,而不需要串口。

Tlenet新建连接,选择Telnet协议,主机名是板卡的ip。

 点击连接,弹出新窗口,输入用户名和密码:

3. 上位机与嵌入式板卡实现文件传输
在SecureCRT中使用命令ifconfig查看板卡的ip地址:

将上位机PC与嵌入式板卡用网线直接连接,将PC机的本地连接ip地址改成与嵌入式板卡同一个网段,例如:

在上位机中打开tftpd32工具,在Server interface中选择上位机与嵌入式板卡相同网段的ip,如下:

注意之类PC机是作为TFTP协议的服务端,板卡作为客户端。

将文件从上位机下载到板卡,在SecureCRT中输入以下命令:

tftp -g -r 1080P.jpg 192.168.1.100


 这里-g表示从PC下载文件到板卡,-r表示远程服务器(即PC)的文件, 1080P.jpg表示要传输的文件,后面的ip地址就是PC机的IP地址,例如:

输入以上命令后,板卡在当前目录下即收到该文件。如果需要在某个目录下接收文件,需要切换到该目录下,再输入以上命令。

将文件从板卡传输到上位机,在SecureCRT中输入以下命令:

tftp -p -l 10801P.jpg 192.168.1.100


这里-p即推送的意思,-l表示本地(板卡)的文件。

4. 常用Linux命令
回到根目录:cd /
解压文件:tar -xvf data.tar
删除文件: rm -f 1.txt
改变目录的读写权限:sudo chmod 777 /usr/share/themes
             说明:这里777表示每个用户都可以读写,后面设的是路径

重命名:mv aaa.h264 bbb.h264
             说明:将aaa.h264重命名为bbb.264

查看ip地址:ifconfig
查看文件属性:stat sample_nnie_main
查看当前路径:pwd
创建文件夹:mkdir
更改ip地址:ifconfig eth0 192.168.6.111  (重启后会丢失)
(持续更新)

5. MP4、AVI等转换为*.h264格式
在嵌入式系统中,程序往往无法直接解码mp4,avi等格式视频文件,需将其转换为裸流.h264格式,方法如下:

PC上下载ffmpeg工具,切换到ffmpeg.exe的目录,用cmd打开命令行窗口:

.mp4转.h264:

ffmpeg -i 1920x1080.mp4 -codec copy -bsf: h264_mp4toannexb -f h264 1920x1080.h264


.avi转.h264:

ffmpeg -i 160x120.avi -vcodec h264 -s 160*120 -an -f m4v test.h264


mov转h264

ffmpeg -i test.mov -vbsf h264_mp4toannexb -vcodec copy -an 2.h264 


即在ffmpeg目录下生成所需格式的文件。

makefile自动化语句
1.自动获取到每个目录

#获取src目录内最大深度为3的所有子目录路径
SRC     := src
ALLSRC  := $(shell find $(SRC) -maxdepth 3 -type d)


2. 自动获取源文件及目标文件

#查找每个SUB_DIRS目录,获取到每个.c,赋值给到SRC
SRC := $(foreach dir, $(SUB_DIRS), $(wildcard $(dir)/*.c))
#自动获取到源文件对应的.o文件
OBJ := $(SRC:%.c=%.o)
 
CPPSRC := $(foreach dir, $(SUB_DIRS), $(wildcard $(dir)/*.cpp))
CPPOBJ := $(CPPSRC:%.cpp=%.o)


3. 在每个头文件目录路径名前加-I

CINCLUDES    := $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))


4. 每个库文件目录路径名前加-L

CLIBS        := $(patsubst %,-L%, $(LIBDIRS:%/=%))


5. 静态(自动化)模式

下面代码中的第1行,$(OBJ)表示取目标名,%.o: %.c表示取目标中.o后缀的文件替换为.c,作为依赖文件。第2行的$<是指第1行中完成替换后的.c文件

$(OBJ): %.o: %.c
    $(CC) -c $< -o $@  $(INC_FLAGS) $(CFLAGS)


以上代码可简写为:

%.o: %.c
    $(CC) -c $< -o $@  $(INC_FLAGS) $(CFLAGS)

一.嵌入式开发必备基础知识
1. 嵌入式系统的组成
嵌入式系统通常由以下几个部分组成:

硬件平台:包括微处理器(如ARM、MIPS、x86等)、传感器、执行器、输入输出设备(如LCD、按键、LED等)。
操作系统:嵌入式系统可以使用实时操作系统(RTOS)或裸机(bare-metal)开发。RTOS如FreeRTOS、uC/OS-II等,裸机编程通常指直接与硬件打交道,没有操作系统的介入。
软件:包括驱动程序、应用程序、系统软件等。驱动程序负责硬件与软件的通信,应用程序则实现系统功能。

2. 嵌入式开发工具链
嵌入式开发通常需要一系列的开发工具:

集成开发环境(IDE):常用的IDE有Keil、IAR Embedded Workbench、Eclipse等,它们提供了代码编辑、编译、调试等功能。
编译器:常见的嵌入式编译器有GCC、ARM Compiler等,能够将源代码编译成适合嵌入式平台的机器代码。
调试器:如JTAG调试器、ST-Link、OCD等,用于调试程序的执行,帮助开发者查看寄存器、内存等信息,实时诊断问题。
仿真器:帮助开发者在没有实际硬件的情况下测试代码。

3. 嵌入式编程语言
嵌入式开发中,最常用的编程语言是:

C语言:几乎所有嵌入式开发都使用C语言,因为它能够直接操作硬件,提供较高的执行效率,并且占用内存较少。嵌入式开发中,C语言常用于编写驱动、操作系统和应用层代码。
汇编语言:对于需要极高性能和硬件控制的任务,汇编语言有时用于优化代码,直接操作硬件寄存器。
C++:在一些复杂的嵌入式系统中,C++用于面向对象编程,尤其是在处理较为复杂的算法时。

4. 嵌入式开发中的实时性要求
实时性是嵌入式系统中至关重要的概念,特别是在处理信号采集、控制系统时。根据实时性要求,嵌入式系统可以分为:

硬实时系统:对时间要求非常严格,任务必须在规定的时间内完成,否则将导致系统失败。例如,航空航天、医疗设备等。
软实时系统:虽然有时间限制,但如果超时不会导致系统完全失败,系统仍然能正常工作。例如,视频播放、音频处理等。

5. 基本的嵌入式开发流程
嵌入式开发流程一般包括以下步骤:

需求分析:明确系统的功能需求、硬件需求、性能要求等。
硬件选择:选择适合的微控制器(MCU)或微处理器(MPU),并了解其硬件资源(如GPIO、UART、SPI、I2C等外设)。
软件设计:根据需求设计嵌入式软件架构,包括驱动、RTOS配置、应用层逻辑等。
编程与调试:在开发环境中编写代码,进行调试和测试,确保软件的正确性和性能。
测试与验证:在目标硬件上进行系统测试,验证软件和硬件的协同工作。

6. 简单的嵌入式代码示例
下面我们将通过一个简单的LED闪烁的例子,展示嵌入式编程的基本内容。我们使用C语言在STM32微控制器上编写代码,来控制一个LED灯每秒闪烁一次。

硬件连接:
STM32开发板
连接到板上的LED(一般连接到某个GPIO口)

软件代码:

#include "stm32f4xx.h"
 
// 初始化GPIO
void GPIO_Init(void)
{
    // 打开GPIO时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN;  // 开启GPIOD时钟
 
    // 配置GPIO模式为输出
    GPIOG->MODER &= ~(0x3 << (13 * 2));  // 清除GPIO口13的模式
    GPIOG->MODER |= (0x1 << (13 * 2));   // 设置GPIO口13为输出模式
}
 
// 延时函数
void Delay(uint32_t delay)
{
    while (delay--) {
        __NOP();  // 空操作,消耗时间
    }
}
 
// 主函数
int main(void)
{
    // 初始化GPIO
    GPIO_Init();
 
    // 主循环
    while (1) {
        // 点亮LED
        GPIOG->ODR |= (1 << 13);  // 设置GPIO口13为高电平,LED亮
        Delay(1000000);            // 延时
 
        // 熄灭LED
        GPIOG->ODR &= ~(1 << 13);  // 设置GPIO口13为低电平,LED灭
        Delay(1000000);            // 延时
    }
}


代码解析:
GPIO初始化:使用GPIO_Init函数初始化GPIO端口,将端口13配置为输出模式。STM32的GPIO寄存器通过MODER配置端口的工作模式。
延时函数:Delay函数通过循环实现一个简单的延时,模拟延时1秒钟。
主循环:在main函数的主循环中,我们交替控制LED的亮灭状态,利用ODR寄存器设置GPIO口的输出值。

7.更复杂的嵌入式开发代码示例
我们将通过一个稍微复杂的示例来展示如何编写嵌入式代码,控制多个外设,并结合中断来实现更高效的系统响应。

目标
控制一个LED闪烁,频率为1Hz。
使用定时器中断(Timer Interrupt)来控制LED闪烁。
硬件需求
STM32F4开发板
LED:连接到GPIO口
定时器:使用系统定时器定时产生中断
系统架构
使用STM32的定时器产生中断。
每次定时器中断发生时,改变LED的状态(开/关)。
代码实现
1. 初始化GPIO口
我们首先需要初始化GPIO口,以便控制LED的开关。假设LED连接到GPIO端口的第13引脚(常见于STM32F4开发板的内置LED)。

#include "stm32f4xx.h"
 
// 初始化GPIO口
void GPIO_Init(void)
{
    // 开启GPIOG时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN;  // 开启GPIOG的时钟
 
    // 设置GPIOG13为输出模式
    GPIOG->MODER &= ~(0x3 << (13 * 2));  // 清除原有的设置
    GPIOG->MODER |= (0x1 << (13 * 2));   // 设置为输出模式
}


2. 初始化定时器
为了产生定时器中断,我们需要配置STM32的定时器。定时器将根据我们设置的预分频值和自动重载值(ARR)来产生中断。

// 定时器初始化
void Timer_Init(void)
{
    // 开启定时器6时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;   // 开启TIM6时钟
 
    // 配置定时器6的时钟频率
    TIM6->PSC = 16000 - 1;  // 设置预分频值,1秒产生一次中断
    TIM6->ARR = 1000 - 1;   // 设置自动重载值,定时器每1秒产生一次中断
 
    // 使能定时器中断
    TIM6->DIER |= TIM_DIER_UIE;   // 使能更新中断
 
    // 启动定时器
    TIM6->CR1 |= TIM_CR1_CEN;     // 启动定时器
}


 3. 中断处理程序
当定时器溢出时,它会产生一个中断,触发中断服务程序。我们需要在中断服务程序中实现LED的状态切换。

// 中断服务程序
void TIM6_DAC_IRQHandler(void)
{
    if (TIM6->SR & TIM_SR_UIF) {  // 检查更新中断标志
        TIM6->SR &= ~TIM_SR_UIF;  // 清除中断标志
 
        // 切换LED状态
        GPIOG->ODR ^= (1 << 13);  // 切换GPIO13的输出状态(开关LED)
    }
}


4. 主函数
主函数中,初始化GPIO和定时器,并开启全局中断。

int main(void)
{
    // 初始化GPIO和定时器
    GPIO_Init();
    Timer_Init();
 
    // 配置NVIC,使能定时器6的中断
    NVIC_EnableIRQ(TIM6_DAC_IRQn);  // 启用定时器6的中断请求
 
    // 使能全局中断
    __enable_irq();  // 开启全局中断
 
    // 主循环(空循环,所有工作由中断处理)
    while (1) {
        // 这里不需要做任何事情,LED控制完全由定时器中断处理
    }
}


5. 总结与扩展
GPIO控制:我们通过寄存器直接控制GPIO端口,设置为输出模式,通过ODR寄存器控制LED的开关。
定时器中断:定时器用于产生精确的中断,并在中断服务程序中处理LED状态的切换。定时器配置涉及到预分频器和自动重载值。
中断服务程序:通过TIM6_DAC_IRQHandler来处理定时器中断,每当定时器溢出时切换LED状态。
6. 代码扩展
外部中断:如果你希望使用按钮等外部设备控制LED状态,可以设置GPIO外部中断。 

// 初始化外部中断
void EXTI_Init(void)
{
    // 使能GPIO端口时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
 
    // 配置PA0为输入模式
    GPIOA->MODER &= ~(0x3 << 0);  // 清除PA0的模式
    GPIOA->PUPDR |= (0x1 << 0);   // 设置为上拉输入模式
 
    // 配置外部中断
    SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;  // 设置PA0为外部中断0的来源
    EXTI->IMR |= EXTI_IMR_MR0;  // 使能外部中断0
    EXTI->RTSR |= EXTI_RTSR_TR0;  // 配置为上升沿触发
 
    // 使能中断控制器
    NVIC_EnableIRQ(EXTI0_IRQn);  // 启用外部中断0的中断请求
}
 
// 外部中断0的中断处理函数
void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0) {  // 检查是否为外部中断0
        EXTI->PR |= EXTI_PR_PR0;  // 清除中断标志
 
        // 切换LED状态
        GPIOG->ODR ^= (1 << 13);  // 切换GPIO13的输出状态
    }
}


二.常见的嵌入式C语言知识
在嵌入式开发中,C语言是最常用的编程语言之一,因为它能够直接操作硬件、实现高效的底层控制,并且与许多嵌入式系统平台兼容。嵌入式C开发通常需要具备一些特殊的知识和技能,尤其是在资源受限的环境中。下面,我将介绍一些常见的嵌入式C语言知识,并夹杂一些代码示例,帮助你理解如何在嵌入式系统中应用这些知识。

1. 指针和内存操作
在嵌入式开发中,直接操作硬件寄存器和内存地址是常见的任务。指针用于存储地址,并可以直接访问存储在特定内存位置的数据。例如,嵌入式系统中的外设控制寄存器通常是通过指针直接访问的。

示例代码:访问硬件寄存器
假设我们要控制一个LED的GPIO引脚,使用指针操作硬件寄存器。

#define LED_GPIO_BASE 0x40020000  // 假设GPIOA的基地址
#define GPIO_MODER_OFFSET 0x00    // GPIO模式寄存器的偏移
#define GPIO_ODR_OFFSET  0x14    // GPIO输出数据寄存器的偏移
 
volatile uint32_t *gpio_moder = (volatile uint32_t *)(LED_GPIO_BASE + GPIO_MODER_OFFSET);
volatile uint32_t *gpio_odr   = (volatile uint32_t *)(LED_GPIO_BASE + GPIO_ODR_OFFSET);
 
void LED_Init(void) {
    // 设置PA5为输出模式
    *gpio_moder &= ~(0x3 << (5 * 2));  // 清除原有设置
    *gpio_moder |= (0x1 << (5 * 2));   // 设置为输出模式
}
 
void LED_Toggle(void) {
    *gpio_odr ^= (1 << 5);  // 切换PA5的状态
}


在这个例子中,我们使用了指针来访问GPIO寄存器。volatile关键字确保编译器不会优化掉对这些寄存器的访问,因为它们是硬件相关的,可能会在程序运行时发生变化。

2. 中断和中断服务程序(ISR)
嵌入式系统中,中断是一个重要的概念,它允许CPU在处理完当前任务后,响应外部事件。中断服务程序(ISR)是响应中断事件时执行的代码。

示例代码:定时器中断
在一个简单的定时器中断示例中,我们使用定时器中断来每隔一段时间切换LED的状态。

#include "stm32f4xx.h"
 
// 定时器6中断服务程序
void TIM6_DAC_IRQHandler(void) {
    if (TIM6->SR & TIM_SR_UIF) {  // 检查定时器更新中断标志
        TIM6->SR &= ~TIM_SR_UIF;  // 清除中断标志
 
        // 切换LED状态
        GPIOG->ODR ^= (1 << 13);  // 切换GPIO13(LED)
    }
}
 
void Timer_Init(void) {
    // 启用定时器6时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
 
    // 配置定时器6
    TIM6->PSC = 16000 - 1;  // 设置预分频器,定时器溢出时间为1秒
    TIM6->ARR = 1000 - 1;   // 自动重载值,设定溢出周期为1秒
 
    // 启用定时器中断
    TIM6->DIER |= TIM_DIER_UIE;
 
    // 启动定时器
    TIM6->CR1 |= TIM_CR1_CEN;
 
    // 启用定时器中断向量
    NVIC_EnableIRQ(TIM6_DAC_IRQn);
}
 
int main(void) {
    // 初始化GPIO和定时器
    GPIO_Init();
    Timer_Init();
 
    // 启用全局中断
    __enable_irq();
 
    // 无限循环,所有工作通过中断完成
    while (1) {
        // 主循环中不做任何事情
    }
}


在中断服务程序中,我们通过TIM6_DAC_IRQHandler函数来响应定时器中断,切换LED的状态。TIM6定时器被配置为每1秒触发一次中断。

3. 位操作(Bit Manipulation)
在嵌入式开发中,我们经常需要进行位操作,以直接操作寄存器或者控制某些硬件外设的状态。C语言提供了很多位操作的工具,如移位、与、或、非等操作。

示例代码:位操作控制GPIO
假设我们要设置一个GPIO端口的某一位为输出(如设置GPIO引脚5为输出)。

#define GPIOA_MODER  ((volatile uint32_t *) 0x40020000) // GPIOA模式寄存器地址
#define GPIOA_ODR    ((volatile uint32_t *) 0x40020014) // GPIOA输出数据寄存器地址
 
void GPIOA_SetPin5Output(void) {
    // 设置GPIOA引脚5为输出模式
    *GPIOA_MODER &= ~(0x3 << (5 * 2));   // 清除原设置
    *GPIOA_MODER |= (0x1 << (5 * 2));    // 设置为输出模式
}
 
void GPIOA_SetPin5High(void) {
    *GPIOA_ODR |= (1 << 5);  // 设置PA5为高电平
}
 
void GPIOA_SetPin5Low(void) {
    *GPIOA_ODR &= ~(1 << 5);  // 设置PA5为低电平
}


4. 定时器和延时
嵌入式开发中常用定时器进行精确延时。虽然使用外部时钟和定时器来产生延时更加精准,但也有一些简单的基于循环的延时方式,适用于资源受限的场合。

示例代码:基于定时器的延时
在一些简单的系统中,如果不使用操作系统,可以使用定时器进行延时。

void delay_ms(uint32_t ms) {
    // 假设系统时钟是168MHz,我们配置一个定时器进行延时
    TIM2->PSC = 16799;     // 设置预分频器,168MHz / 16800 = 10kHz
    TIM2->ARR = 999;        // 设置定时器重载值,10kHz * 1000 = 1秒
 
    TIM2->CNT = 0;          // 清除计数器
    TIM2->CR1 |= TIM_CR1_CEN;  // 启动定时器
 
    for (uint32_t i = 0; i < ms; i++) {
        while (!(TIM2->SR & TIM_SR_UIF));  // 等待定时器溢出
        TIM2->SR &= ~TIM_SR_UIF;          // 清除溢出标志
    }
 
    TIM2->CR1 &= ~TIM_CR1_CEN;  // 关闭定时器
}


5. 优化与资源管理
在嵌入式开发中,硬件资源通常非常有限(如内存和处理能力)。因此,需要对代码进行优化,减少内存占用和CPU使用,确保程序高效运行。

示例代码:优化内存使用
一个常见的优化技巧是避免使用递归和过多的堆栈空间,而是通过循环和静态分配来实现。

#define BUFFER_SIZE 128
uint8_t buffer[BUFFER_SIZE]; // 静态内存分配,避免动态内存分配
 
void process_data(void) {
    for (int i = 0; i < BUFFER_SIZE; i++) {
        buffer[i] = i * 2;  // 填充数据
    }
}


 三.完整的嵌入式开发项目
下面是一个简单的嵌入式项目代码示例,使用了STM32系列微控制器来控制一个LED灯,通过定时器中断每隔1秒切换LED的状态。该示例包含了GPIO初始化、定时器配置、和中断服务程序(ISR)的使用。

项目需求:
配置一个GPIO引脚(如PA5)控制一个LED灯的开关。
使用定时器(比如TIM6)每隔1秒切换一次LED状态。
使用中断服务程序(ISR)响应定时器中断,并切换LED状态。
使用的硬件:
STM32F4系列开发板
连接至PA5引脚的LED(如STM32F4的板载LED)
项目结构:
main.c:主要代码文件,包含初始化、主循环和定时器中断服务程序。
startup.s:启动文件,通常由STM32的HAL库自动生成或手动编写。
main.c 完整代码:

#include "stm32f4xx.h"
 
// LED控制引脚PA5
#define LED_PIN     5
#define GPIOA_BASE  0x40020000  // GPIOA基地址
#define GPIOA_MODER 0x00        // GPIOA模式寄存器偏移
#define GPIOA_ODR   0x14        // GPIOA输出数据寄存器偏移
 
// 定时器6中断服务程序
void TIM6_DAC_IRQHandler(void) {
    if (TIM6->SR & TIM_SR_UIF) {  // 检查定时器更新中断标志
        TIM6->SR &= ~TIM_SR_UIF;  // 清除中断标志
 
        // 切换PA5的状态(LED)
        GPIOA->ODR ^= (1 << LED_PIN);  // 切换LED的状态
    }
}
 
// GPIO初始化,配置PA5为输出
void GPIO_Init(void) {
    // 启用GPIOA时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
 
    // 设置PA5为输出模式
    GPIOA->MODER &= ~(0x3 << (LED_PIN * 2));  // 清除原有设置
    GPIOA->MODER |= (0x1 << (LED_PIN * 2));   // 设置为输出模式(00: 输入, 01: 输出, 10: 复用, 11: 模式)
}
 
// 定时器6初始化,每秒中断一次
void Timer_Init(void) {
    // 启用定时器6时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
 
    // 设置定时器预分频器,假设系统时钟为168MHz,定时器6的频率为1kHz
    TIM6->PSC = 16799;  // 168MHz / 16800 = 10kHz
    TIM6->ARR = 999;     // 设置重载值,定时器溢出周期为1秒
 
    // 启用定时器中断
    TIM6->DIER |= TIM_DIER_UIE;
 
    // 启动定时器
    TIM6->CR1 |= TIM_CR1_CEN;
 
    // 启用定时器中断向量
    NVIC_EnableIRQ(TIM6_DAC_IRQn);
}
 
int main(void) {
    // 初始化GPIO和定时器
    GPIO_Init();
    Timer_Init();
 
    // 启用全局中断
    __enable_irq();
 
    // 无限循环,主循环中不做任何事情,所有工作由中断完成
    while (1) {
        // 主循环为空,因为LED的切换工作完全交给中断处理
    }
}


代码解析:
GPIO初始化:

GPIO_Init() 函数负责配置PA5为输出模式。我们首先启用GPIOA时钟,然后通过MODER寄存器设置PA5为输出模式。
定时器初始化:

Timer_Init() 函数负责配置定时器6(TIM6)。在此示例中,定时器6的时钟源为APB1总线时钟,假设系统时钟为168 MHz。通过设置PSC(预分频器)和ARR(自动重载值),我们配置定时器的周期为1秒(溢出周期为1000ms)。
启用定时器中断,当定时器溢出时,会触发TIM6_DAC_IRQHandler()中断服务程序。
定时器中断服务程序(ISR):

在TIM6_DAC_IRQHandler()函数中,我们首先检查是否发生了定时器溢出中断。如果是,清除中断标志,并切换LED的状态(通过GPIOA->ODR寄存器操作PA5的输出状态)。
主循环:

主循环实际上不做任何工作,所有LED切换的任务都由定时器中断来处理。由于中断处理程序会定期切换LED状态,因此主循环为空。
全局中断启用:

__enable_irq() 函数启用全局中断,使定时器中断能够触发并执行。
项目构建:
启动文件和链接器脚本:

在STM32开发中,通常会使用标准的启动文件(startup_stm32f4xx.s)来配置中断向量表和复位启动流程。链接器脚本(如STM32F4.x.ld)会指定内存布局,确保代码和数据存储在正确的位置。
编译工具链:

该项目需要使用ARM工具链(如gcc-arm-none-eabi)进行编译。你可以通过STM32CubeIDE或Keil等IDE进行项目的编译和烧录。
硬件配置:

确保LED正确连接到PA5引脚,并且相应的电阻已经正确配置。你可以使用开发板的板载LED,或者通过外接LED与适当的电阻连接。
运行效果:
每1秒钟,定时器会产生一次中断,触发TIM6_DAC_IRQHandler()函数,切换PA5的状态,从而使连接到PA5的LED闪烁。

 四.总结
嵌入式开发是将计算机技术嵌入到硬件设备中的一种开发模式,广泛应用于消费电子、汽车、工业控制、医疗设备、智能家居等领域。它不仅涉及到传统的软件开发,还包括对硬件平台的深入理解。嵌入式系统通常具有资源受限(如内存、存储、处理能力、功耗等)的特点,因此开发者需要在高效利用硬件资源的基础上,确保系统的稳定性、实时性和可靠性。嵌入式开发涉及多个层次,从硬件设计到驱动开发,再到上层应用的实现,需要开发者掌握微处理器架构、数字电路、操作系统(RTOS或裸机编程)、设备驱动、通信协议、调试工具等多方面的知识。

在实际开发过程中,开发者需要与硬件工程师紧密合作,理解硬件电路设计、信号处理等,以确保软件能够有效控制硬件工作。同时,嵌入式开发中常常需要处理外部设备的接口与通信问题,如GPIO、UART、SPI、I2C等通信协议的实现,这要求开发者能够灵活运用中断、DMA、定时器等硬件特性。在系统软件方面,嵌入式开发通常涉及到底层固件(如启动引导程序)、驱动程序和应用层代码的编写,尤其是在实时性要求较高的场合,还需要设计高效的任务调度和资源管理机制。

调试和优化是嵌入式开发中的关键环节,由于嵌入式设备大多没有显示器或操作界面,开发者需要借助串口调试、JTAG、逻辑分析仪、示波器等工具进行硬件级调试,进行代码的性能分析和优化。此外,功耗管理也是嵌入式系统中的一个重要课题,如何在保证系统性能的前提下降低功耗,延长电池使用寿命,是开发者在设计嵌入式系统时必须考虑的因素。

总的来说,嵌入式开发是一个跨学科的领域,既需要硬件知识,也需要扎实的软件编程能力,开发者要不断学习新技术、理解硬件与软件的紧密配合,才能在复杂的嵌入式系统中成功实现创新应用。

   从结构图和数据手册得知,看门狗Watchdog主要是实现系统自动复位的功能,它是利用芯片内部的定时器,定时输出连接到电路的复位端,程序在一定时间范围内对定时器清零(俗称“喂狗”),当程序在正常工作时,定时器总是不能溢出,也就不能产生复位信号;一旦程序出现错误,可能就无法在定时周期内复位看门狗,那么定时器就会溢出而产生复位信号使系统复位。

     S3C2440的Watchdog模块提供了三个寄存器来对Watchdog进行操作,他们分别是:定时器控制寄存器WTCON、定时器数据寄存器WTDAT和定时器计数寄存器WTCNT。注意:在对定时器数据寄存器WTDAT进行操作时必须在定时器控制寄存器WTCON使能之前写入一个计数目标值,当Watchdog使能开启后,WTDAT中的值会自动被加载到计数寄存器WTCNT中,然后Watchdog从CPU内部的时钟分频和时钟除数因子得到一个工作周期,当每个周期结束时计数寄存器WTCNT中的值会1,直到递减为0时,如果还不重新往WTCNT中写入新的计数目标值(即“喂狗”),则 Watchdog就产生复位信号使系统复位。

   了解了大致的硬件原理后,咱们就开始结合驱动程序源代码,来讲解有关看门狗驱动程序的写法。

//必要的头文件
#define S3C_VA_WATCHDOG (0)
#define PFX "s3c2410-wdt:"
#define CONFIG_S3C2410_WATCHDOG_ATBOOT  (0)
#define CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME  (15)
static int nowayout = WATCHDOG_NOWAYOUT;
//nowayout与CONFIG_WATCHDOG_NOWAYOUT配置相关,其一旦配置应用层调用close函数将不能关闭看门狗
static int tmr_margin = CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME;//默认的喂狗时间
static int tmr_atboot = CONFIG_S3C2410_WATCHDOG_ATBOOT;
//关于tmr_atboot,要想系统一上电自动使能看门狗,就为1。最好使用0,由于我们想通过open函数来打开看门狗
static int soft_noboot;
//soft_noboot为1时看门狗将作为一般的中断定时器使用,为0时作为可复位电路的看门狗,默认为0
static int debug;  调式模式
/*驱动程序模块参数,如果在加载驱动模块时没有设定这些参数,则这些参数将采用默认值,
这些参数在接下来的步骤中将被一一用到,参数具体作用也将在各步骤中来说明*/
module_param(tmr_margin, int, 0);
module_param(tmr_atboot, int, 0);
module_param(nowayout, int, 0);
module_param(soft_noboot, int, 0);
module_param(debug, int, 0);
//下面的这些没什么大的作用,就是为了做参数描述
MODULE_PARM_DESC(tmr_margin, "Watchdog tmr_margin in seconds. default="
__MODULE_STRING(CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME) ")");
MODULE_PARM_DESC(tmr_atboot, "Watchdog is started at boot time if set to 1, default="
__MODULE_STRING(CONFIG_S3C2410_WATCHDOG_ATBOOT));
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
__MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
MODULE_PARM_DESC(soft_noboot, "Watchdog action, set to 1 to ignore reboots, "
"0 to reboot (default depends on ONLY_TESTING)");
MODULE_PARM_DESC(debug, "Watchdog debug, set to >1 for debug, (default 0)");
static unsigned long open_lock;
static struct device *wdt_dev;    /* platform device attached to */
//用来保存watchdog的IO端口占用的IO空间和经过虚拟映射后的内存地址
static struct resource *wdt_mem;
static void __iomem *wdt_base;
static struct resource *wdt_irq;
//保存从平台时钟队列中获取watchdog的时钟
static struct clk *wdt_clock;
//用于保存经计算后得到的计数寄存器WTCNT的计数值
static unsigned int wdt_count;
static char expect_close;
static DEFINE_SPINLOCK(wdt_lock);
/* watchdog control routines */
#define DBG(msg...) do { \ //打印调试信息
if (debug) printk(KERN_INFO msg); } while (0)
/* functions */
static void s3c2410wdt_keepalive(void) //喂狗
{
spin_lock(&wdt_lock);//获取自旋锁保护临界区资源 
writel(wdt_count, wdt_base + S3C2410_WTCNT);//往计数寄存器WTCNT重新写入计数值
spin_unlock(&wdt_lock);//释放自旋锁
}
static void __s3c2410wdt_stop(void)
{
unsigned long wtcon;
//停止看门狗定时器
wtcon = readl(wdt_base + S3C2410_WTCON);
wtcon &= ~(S3C2410_WTCON_ENABLE | S3C2410_WTCON_RSTEN);
writel(wtcon, wdt_base + S3C2410_WTCON);
}
static void s3c2410wdt_stop(void)
{
spin_lock(&wdt_lock);//获取自旋锁保护临界区资源
__s3c2410wdt_stop();
spin_unlock(&wdt_lock);//释放自旋锁,即解锁
}
static void s3c2410wdt_start(void)
{
unsigned long wtcon;
spin_lock(&wdt_lock);
__s3c2410wdt_stop();
wtcon = readl(wdt_base + S3C2410_WTCON);
wtcon |= S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128;
if (soft_noboot) {
wtcon |= S3C2410_WTCON_INTEN;//该为1时,看门狗为一般定时器产生中断
wtcon &= ~S3C2410_WTCON_RSTEN;//重启无效
} else {
wtcon &= ~S3C2410_WTCON_INTEN;//默认为可复位
wtcon |= S3C2410_WTCON_RSTEN;
}
DBG("%s: wdt_count=0x%08x, wtcon=%08lx\n",__func__, wdt_count, wtcon);
writel(wdt_count, wdt_base + S3C2410_WTDAT);
writel(wdt_count, wdt_base + S3C2410_WTCNT);
writel(wtcon, wdt_base + S3C2410_WTCON);
spin_unlock(&wdt_lock);
}
//计算并设置看门狗定时器时钟周期值并初始化看门狗定时器 
static int s3c2410wdt_set_heartbeat(int timeout)
{
unsigned int freq = clk_get_rate(wdt_clock);
unsigned int count;
unsigned int divisor = 1;
unsigned long wtcon;
if (timeout < 1)
return -EINVAL;
freq /= 128;
count = timeout * freq;
DBG("%s: count=%d, timeout=%d, freq=%d\n",__func__, count, timeout, freq);
if (count >= 0x10000) {
for (divisor = 1; divisor <= 0x100; divisor++) {
if ((count / divisor) < 0x10000)
break;
}
        if ((count / divisor) >= 0x10000) {
dev_err(wdt_dev, "timeout %d too big\n", timeout);
return -EINVAL;
}
}
tmr_margin = timeout;
DBG("%s: timeout=%d, divisor=%d, count=%d (%08x)\n",__func__, timeout, divisor, count, count/divisor);
count /= divisor;
wdt_count = count;
//看数据手册得到,wtcon=1000000000100001 这是控制寄存器的默认值
wtcon = readl(wdt_base + S3C2410_WTCON);
wtcon &= ~S3C2410_WTCON_PRESCALE_MASK;
/*wtcon=1000000000100001 & ~1111111100000000 = 0000000000100001
S3C2410_WTCON_PRESCALE宏是将divisor-1的值向左位移8位,也就是说该值的右8位都为0,则计算如下:
wtcon=0000000000100001 | (xxxxxxxx)00000000 = (xxxxxxxx)00100001
*/
wtcon |= S3C2410_WTCON_PRESCALE(divisor-1);
//设置看门狗定时器数据寄存器WTDAT的值,然后WTDAT的值会自动加载到WTCNT中 
writel(count, wdt_base + S3C2410_WTDAT);
writel(wtcon, wdt_base + S3C2410_WTCON);
return 0;
}
//看门狗设备驱动的打开接口函数
static int s3c2410wdt_open(struct inode *inode, struct file *file)
{
if (test_and_set_bit(0, &open_lock))//试着获取信号量(即:加锁),如果获取不成功,说明其他进程此时占用了,就返回忙
return -EBUSY;
    if (nowayout) //如果内核配置了CONFIG_WATCHDOG_NOWAYOUT项,则使模块使用计数加1
__module_get(THIS_MODULE);
expect_close = 0;
//开启看门狗定时器
s3c2410wdt_start();
//表示返回的这个设备文件是不可以被seek操作的,nonseekable_open定义在fs.h中
return nonseekable_open(inode, file);
}
//看门狗设备驱动的关闭接口函数
static int s3c2410wdt_release(struct inode *inode, struct file *file)
{
// 如果判断到当前操作状态是可以关闭看门狗定时器时就关闭,否则就是“喂狗”状态
if (expect_close == 42)
s3c2410wdt_stop();
else {
dev_err(wdt_dev, "Unexpected close, not stopping watchdog\n");
s3c2410wdt_keepalive();//喂狗
}
expect_close = 0;
clear_bit(0, &open_lock);
return 0;
}
static ssize_t s3c2410wdt_write(struct file *file, const char __user *data,
size_t len, loff_t *ppos)
{
//刷新定时器
if (len) {
if (!nowayout) {
size_t i;
/* In case it was set long ago */
expect_close = 0;
for (i = 0; i != len; i++) {
char c;
if (get_user(c, data + i))
return -EFAULT;
if (c == 'V')//写入字符 V容许关闭看门狗,但前提还是不配置CONFIG_WATCHDOG_NOWAYOUT
expect_close = 42;
}
}
s3c2410wdt_keepalive();//喂狗
}
return len;
}
//用于支持看门狗IO控制中获取看门狗信息的命令WDIOC_GETSUPPORT,下面的宏和看门狗信息结构体定义在watchdog.h中
#define OPTIONS (WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE)
static const struct watchdog_info s3c2410_wdt_ident = {
.options = OPTIONS,
.firmware_version = 0,
.identity = "S3C2410 Watchdog",
};
//看门狗设备驱动的IO控制接口函数
static long s3c2410wdt_ioctl(struct file *file,    unsigned int cmd, unsigned long arg)
{
void __user *argp = (void __user *)arg;
int __user *p = argp;
int new_margin;
//以下对看门狗定时器IO控制的命令定义在watchdog.h中
switch (cmd) {
case WDIOC_GETSUPPORT://获取看门狗的支持信息,wdt_ident定义在上面
return copy_to_user(argp, &s3c2410_wdt_ident,sizeof(s3c2410_wdt_ident)) ? -EFAULT : 0;
case WDIOC_GETSTATUS://获取看门狗状态
case WDIOC_GETBOOTSTATUS:
return put_user(0, p);
case WDIOC_KEEPALIVE://喂狗命令
s3c2410wdt_keepalive();
return 0;
case WDIOC_SETTIMEOUT://设置定时器溢出时间值命令(以秒为单位)
if (get_user(new_margin, p))//获取时间值
return -EFAULT;
if (s3c2410wdt_set_heartbeat(new_margin))//设置到计数寄存器WTCNT中
return -EINVAL;
s3c2410wdt_keepalive();//喂狗
return put_user(tmr_margin, p);
case WDIOC_GETTIMEOUT://读取定时器默认溢出时间值命令
return put_user(tmr_margin, p);
default:
return -ENOTTY;
}
}
//字符设备的相关操作实现
static const struct file_operations s3c2410wdt_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,//定义为不可定位,即屏蔽seek操作,no_llseek定义在fs.h中
.write = s3c2410wdt_write,
.unlocked_ioctl = s3c2410wdt_ioctl,
.open = s3c2410wdt_open,
.release = s3c2410wdt_release,
};
//混杂设备结构体
static struct miscdevice s3c2410wdt_miscdev = {
.minor  = WATCHDOG_MINOR,//WATCHDOG_MINOR为次设备号定义在miscdevice.h中为130
.name = "watchdog", //设备名称
.fops = &s3c2410wdt_fops,//实现字符设备的相关操作
};
//看门狗定时器中断服务程序
static irqreturn_t s3c2410wdt_irq(int irqno, void *param)
{
dev_info(wdt_dev, "watchdog timer expired (irq)\n");
//主要要做的事情是在看门狗定时器计数寄存器值递减到0之前重新写入新值(即:“喂狗”)
s3c2410wdt_keepalive();
return IRQ_HANDLED;
}
/* device interface */
static int __devinit s3c2410wdt_probe(struct platform_device *pdev)
{
struct resource *res; //定义一个资源,用来保存获取的watchdog的IO资源
struct device *dev;
unsigned int wtcon;
int started = 0;
int ret;
int size;
//DBG一般在其出现的文件的开始部分,作为打印debug信息用.这句话实现打印它所在函数的函数名称
DBG("%s: probe=%p\n", __func__, pdev);
dev = &pdev->dev;
wdt_dev = &pdev->dev;
//获取watchdog平台设备所使用的IO端口资源,注意这个IORESOURCE_MEM标志和watchdog平台设备定义中的一致
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res == NULL) {
dev_err(dev, "no memory resource specified\n");
return -ENOENT;
}
size = (res->end - res->start) + 1;
//request_mem_region 标记这段物理地址自己使用了,其他人不要使用而已,也不是必须的。
wdt_mem = request_mem_region(res->start, size, pdev->name);
if (wdt_mem == NULL) {
dev_err(dev, "failed to get memory region\n");
ret = -ENOENT;
goto err_req;
}
//将watchdog的IO端口占用的这段IO空间映射到内存的虚拟地址,被映射数据的长度由size参数设定。
//该函数的实质是把一块物理区域二次映射到一个可以从驱动程序里访问的虚拟地址上去。
wdt_base = ioremap(res->start, size);
if (wdt_base == NULL) {
dev_err(dev, "failed to ioremap() region\n");
ret = -EINVAL;
goto err_req;
}
DBG("probe: mapped wdt_base=%p\n", wdt_base);
//在系统定义的watchdog平台设备中获取watchdog中断号platform_get_irq定义在platform_device.h中
wdt_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if (wdt_irq == NULL) {///*获取watchdog中断号不成功错误处理
dev_err(dev, "no irq resource specified\n");
ret = -ENOENT;
goto err_map;
}
//申请Watchdog中断服务.中断服务程序为:s3c2410wdt_irq,将Watchdog平台设备pdev做参数传递过去了 
ret = request_irq(wdt_irq->start, s3c2410wdt_irq, 0, pdev->name, pdev);
if (ret != 0) {
dev_err(dev, "failed to install irq (%d)\n", ret);
goto err_map;
}
/*从平台时钟队列中获取watchdog的时钟,看门狗定时器的工作周期是由这个时钟和时钟除数因子得到的。
注意这里的watchdog参数要与系统中定义的时钟名称一致才能获取得到,也就是说,系统必须先定义得
有watchdog。系统的一些时钟定义在arch/arm/plat-s3c24xx/s3c2410-clock.c中
*/
wdt_clock = clk_get(&pdev->dev, "watchdog");
if (IS_ERR(wdt_clock)) {
dev_err(dev, "failed to find watchdog clock source\n");
ret = PTR_ERR(wdt_clock);
goto err_irq;
}
//时钟获取后要使能后才可以使用,clk_enable定义在arch/arm/plat-s3c/clock.c中
clk_enable(wdt_clock);
//通过上面的步骤已经将watchdog的资源都准备好了,下面就开始使用啦 
//这里是计算并设置看门狗定时器时钟周期值,其实这里就是初始化看门狗定时器,wdt_set_heartbeat定义在下面。
if(s3c2410wdt_set_heartbeat(tmr_margin)) {
//调用两次的意思是看能不能设置成期望的值,如果不能就设置默认的值
started = s3c2410wdt_set_heartbeat(CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME);
if (started == 0)
dev_info(dev, "tmr_margin value out of range, default %d used\n",CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME);
else
dev_info(dev, "default timer value is out of range, cannot start\n");
}
//把wdt又注册为一个杂项设备
ret = misc_register(&s3c2410wdt_miscdev);
if (ret) {
dev_err(dev, "cannot register miscdev on minor=%d (%d)\n",
WATCHDOG_MINOR, ret);
goto err_clk;
}
if (tmr_atboot && started == 0) {
dev_info(dev, "starting watchdog timer\n");
s3c2410wdt_start();//如果此时看门狗还没有开启,现在就调用函数开启
} else if (!tmr_atboot) {
s3c2410wdt_stop();
}
wtcon = readl(wdt_base + S3C2410_WTCON);
dev_info(dev, "watchdog %sactive, reset %sabled, irq %sabled\n",
(wtcon & S3C2410_WTCON_ENABLE) ?  " " : "in",
(wtcon & S3C2410_WTCON_RSTEN) ? "" : "dis",
(wtcon & S3C2410_WTCON_INTEN) ? "" : "en");
return 0;
err_clk:
clk_disable(wdt_clock);
clk_put(wdt_clock);
err_irq:
free_irq(wdt_irq->start, pdev);
err_map:
iounmap(wdt_base);
err_req:
release_resource(wdt_mem);
kfree(wdt_mem);
return ret;
}
static int __devexit s3c2410wdt_remove(struct platform_device *dev)
{
release_resource(wdt_mem);//释放获取的watchdog平台设备的IO资源
kfree(wdt_mem);
wdt_mem = NULL;
//同watchdog_probe中中断的申请相对应,在那里申请中断,这里就释放中断
free_irq(wdt_irq->start, dev);
wdt_irq = NULL;
//释放获取的Watchdog平台设备的时钟
clk_disable(wdt_clock);
clk_put(wdt_clock);
wdt_clock = NULL;
//释放Watchdog设备虚拟地址映射空间
iounmap(wdt_base);
//注销misc设备
misc_deregister(&s3c2410wdt_miscdev);
return 0;
}
//Watchdog平台驱动的设备关闭接口函数的实现
static void s3c2410wdt_shutdown(struct platform_device *dev)
{
s3c2410wdt_stop();
}
/*对Watchdog平台设备驱动电源管理的支持。CONFIG_PM这个宏定义在内核中,
当配置内核时选上电源管理,则Watchdog平台驱动的设备挂起和恢复功能均有效,
这时候你应该明白了在第②步中为什么要有device_init_wakeup(&pdev->dev, 1)这句吧*/
#ifdef CONFIG_PM
//定义两个变量来分别保存挂起时的WTCON和WTDAT值,到恢复的时候使用
static unsigned long wtcon_save;
static unsigned long wtdat_save;
//Watchdog平台驱动的设备挂起接口函数的实现
static int s3c2410wdt_suspend(struct platform_device *dev, pm_message_t state)
{
//保存看门狗的状态值,并且关闭它
wtcon_save = readl(wdt_base + S3C2410_WTCON);
wtdat_save = readl(wdt_base + S3C2410_WTDAT);
//停止看门狗定时器 
s3c2410wdt_stop();
return 0;
}
//Watchdog平台驱动的设备恢复接口函数的实现
static int s3c2410wdt_resume(struct platform_device *dev)
{
//恢复看门狗的状态
writel(wtdat_save, wdt_base + S3C2410_WTDAT);
writel(wtdat_save, wdt_base + S3C2410_WTCNT);  //重置寄存器
writel(wtcon_save, wdt_base + S3C2410_WTCON);
printk(KERN_INFO PFX "watchdog %sabled\n",(wtcon_save & S3C2410_WTCON_ENABLE) ? "en" : "dis");
return 0;
}
#else //如果配置内核时没选上电源管理,Watchdog平台驱动的设备挂起和恢复功能均无效,这两个函数也就无需实现了
#define s3c2410wdt_suspend NULL
#define s3c2410wdt_resume  NULL
#endif /* CONFIG_PM */
//Watchdog平台驱动结构体,平台驱动结构体定义在platform_device.h中
static struct platform_driver s3c2410wdt_driver = {
.probe = s3c2410wdt_probe,
.remove = __devexit_p(s3c2410wdt_remove),
.shutdown = s3c2410wdt_shutdown,
.suspend = s3c2410wdt_suspend,
.resume = s3c2410wdt_resume,
//注意这里的名称一定要和系统中定义平台设备的地方一致,这样才能把平台设备与该平台设备的驱动关联起来
.driver = {
.owner = THIS_MODULE,
.name = "s3c2410-wdt",
},
};
static char banner[] __initdata = KERN_INFO "S3C2410 Watchdog Timer, (c) 2004 Simtec Electronics\n";
static int __init watchdog_init(void)
{
printk(banner);
//将Watchdog注册成平台设备驱动
return platform_driver_register(&s3c2410wdt_driver);
}
static void __exit watchdog_exit(void)
{
//注销Watchdog平台设备驱动
platform_driver_unregister(&s3c2410wdt_driver);
}
module_init(watchdog_init);
module_exit(watchdog_exit);
MODULE_AUTHOR("hanyan225");
MODULE_LICENSE("GPL");
MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR); 

左边是我特意给你画的结构,相信有了这个结构图,你就会对整体的架构有一个了解,好了,你可能会问,你说看门狗是字符设备(char device),是平台设备(platform device) ,最后还是混杂设备(miscdevice).那它到底是啥设备,不用你问,下面我就来告诉你,这也是今天的最后一个内容了(看着小王对这几个概念

一头雾水,我也别卖关子了,告诉她得了):曾看到这样一段话,说一粒花生,如果我们把它看作字符设备,当我们煮着炖着吃了,它就成了菜中的一个混杂设备,但是了它总是从挂在花生秧的根部上的一员长来了,因此它是可以作为一个花生秧苗中一个独立的设备而存在,所以就也可以定义为一个平台设备。这说明什么呢?字符设备是对其本质的描述,说明是串行,顺序访问的(而不是缓冲随机的),混杂设备是存放这个字符设备的容器,即这个设备是被丢在使用了同一设备号的混杂设备里面,平台设备则是描述了设备的一种特性或者说属性。换句话说就是这个设备属于平台的独立模块,完全是一种附加的属性。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐