drivers_day03

本文详细介绍了Linux内核编程的基础知识,包括用户空间与内核空间的区分、内核模块的加载与卸载、GPIO操作库函数的应用、系统调用的过程与实现方法。同时,通过实例展示了如何在内核中实现开关灯功能,深入浅出地讲解了Linux内核编程的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

回顾:

1.linux系统分为用户空间和内核空间

用户空间

内核空间

2.linux内核编程的规范

2.1入口函数:module_init

intdriver_entry(void);

2.2出口函数:module_exit

voiddriver_exit(void);

注意:

         1.不能使用标准的C库头文件和C库

         2.不能处理浮点数,一般把浮点数的运算放在用户空间来

2.3内核代码编译

obj-m += xxxx.o

make -C/opt/kernel SUBDIRS=/opt/driver/day02/1.0 modules

2.4内核模块操作

insmod

rmmod

lsmod

modinfo

modprobe

注意:建议搭建在配置busybox时,使用完整版的模块操作命令

模块加载命令insmod和modprobe对比:

insmod仅仅只是加载模块,不会去检查模块的依赖关系,如果使用insmod加载模块,必须人为事先直到模块之间的依赖。并且insmod在加载模块时,必须指定模块所在的路径!

insmod  /home/helloworld.ko

modprobe不仅仅用于加载或者卸载模块,还能够根据modues.dep来检查模块之间的依赖,根据依赖关系,决定先加载哪个模块。并且modprobe加载模块时,默认的模块在/opt/rootfs/lib/module/2.6.35.7../extra,不会到当前目录去找模块!

 

2.5modules.dep依赖文件从何而来?

1.进入内核源码只执行一次make modules即可(module.order)

2.进入自己的驱动模块所在的路径,进行安装模块

   make modules

   make install

Makefile:

install:

      make -C /opt/kernel SUBDIRS=$(PWD)modules_install INSTALL_MOD_PATH=/opt/

结果:在/opt/下生成一个目标目录lib目录,将lib下的内容全部拷贝到根文件系统的lib目录中即可

 

2.6给内核模块添加相关信息

MODULE_LICENSE("GPL");//必须添加

MODULE_AUTHOR("youcw<youcw@tarena.com.cn>"); //作者信息

MODULE_VERSION("1.0.0");//busybox-1.19.4的modinfo命令有问题,需要使用新版的busybox才能获取版本信息。

2.7内核模块参数声明

module_param(name,type,perm)

module_param_array(name,type,nump,perm)

注意:perm:0和非0

2.8内核符号导出

EXPORT_SYMBOL

EXPORT_SYMBOL_GPL

2.9printk内核打印函数

指定了8个输出级别0~7

#defineKERN_WARNING  "<4>"

默认打印输出级别:限制哪些信息打印输出,哪些不做输出

默认打印输出级别的设置方法:

1./proc/sys/kernel/printk

2.uboot的bootargs(quiet,debug,loglevel=数字)

 

**************************************************************************************

Day3:              

一:linux内核提供了GPIO操作的库函数:

内核为了方便驱动操作GPIO,内核封装了一套GPIO操作的库函数,这些函数使用的前提是GPIO为输入或者输出。并且这些库函数的底层硬件寄存器的操作都由芯片公司在平台代码中帮你写好,驱动只需调用库函数操作即可!

 

GPIO的软件编号

芯片手册对于每一个硬件GPIO,都给定一个符号“GPC1_3”或者"GPC1_3",内核对于每一个GPIO也给出了对应的软件标号(本质上就是一个数字),当然每一个GPIO都有唯一的编号!

硬件标识          软件编号

GPC1_3       S5PV210_GPC1(3)

内核提供的GPIO库函数就是通过软件编号来对硬件GPIO进行操作!

 

1)库函数:

gpio_request(软件编号,标签(字符串))

GPIO在操作之前,必须向内核去申请资源!一旦申请成功,别的模块就不能再访问这个资源了。

gpio_direction_output(软件编号,高/低电平)//配置GPIO为输出口,并且输出给定电平,GPIO的控制权由CPU掌管

gpio_direction_input(软件编号);//配置GPIO为输入口,GPIO控制权由外设来掌管

gpio_set_value(软件编号,电平状态); //函数使用的前提是GPIO为输出

gpio_get_value(软件编号); //获取GPIO的状态,GPIO输入输出无要求

gpio_free(软件编号); //如果GPIO资源不再使用,一定要归还给操作系统,供其他模块使用。

gpio的控制权,或者gpio连接的总线的控制权归属问题:

cpu设置gpio为输出,则控制权归cpucpu设置gpio为输入,则控制权归外设!谁设置输出,谁掌握控制权!

头文件:

#include <asm/gpio.h>

#include <plat/gpio-cfg.h>

案例:gpio编程

在加载驱动模块时,点亮灯

在卸载驱动模块时,关闭灯

 

案例:多文件编译

将开关灯操作封装在一个函数中int led_config(int gpio, int value),并且让这个函数在一个C中实现,另外一个C文件来调用这个函数进行对灯操作!

 

提示:

1.led.h

/*

     函数功能:配置操作GPIO

     参数:gpio:软件编号,value:gpio电平状态

*/

extern voidled_config(int gpio, int value);

 

2.led.c

/*函数实现*/

voidled_config(int gpio, int value)

{

      gpio_direction_output

      gpio_set_value

}

EXPORT_SYMBOL_GPL(...);

 

MODULE_LICENSE...

 

3.led_drv.c

 for(...) {

      led_config... //调用

}

 

4.不允许使用insmod,rmmod,必须使用modprobe

******************************************************

二:linux系统调用过程(原理):

1.linux系统给每一个系统调用函数都指定了一个号,这个号称之为系统调用号,例如:exit函数的系统调用号为1,write的系统调用号为4等

2.每当应用程序调用系统调用函数时(例如write函数),首先跑到C库的write函数实现;

3.C库的write函数首先把write函数对应的系统调用号保存在R7寄存器中,然后调用swi或者svc触发一个软中断异常,CPU要处理这个异常,需要跳转到异常向量表的位置去处理。

4.linux系统的异常向量表是在内核启动初始化时会帮你建立好异常向量表(向量表的地址为0xFFFF0000,软中断对应的入口为vector_swi,应用程序触发软中断,那么CPU就会到这个入口去处理软中断;

5.进入软中断vector_swi入口以后,R7寄存器中取出之前保存的系统调用号,然后在内核已经准备好的系统调用表中,以系统调用号为索引(下标)在系统调用表中找到自己对应的函数(sys_write)然后执行此函数(存在于内核空间)。

6.执行完这个函数以后,再原路返回到用户空间!

 

总结:用户空间到内核空间通过软中断来实现!

 

7如何添加一个新的系统调用呢?

内核空间需要完成的工作:

1.arch/arm/include/asm/unistd.h中添加一个新的系统调用号。

#define__NR_OABI_SYSCALL_BASE  0x900000

..........

#define __NR_buzzer_drv                 (__NR_SYSCALL_BASE+366)

2.在arch/arm/kernel/calls.S在这个系统调用表中添加一个新项。

           CALL(sys_buzzer_drv)

3.在内核源码arc/arm/kernel/sys_arm.c添加一个内核函数的实现,类似sys_write

用户空间要完成的工作:

1.linux系统提供了一个系统调用函数syscall来帮你完成保存系统调用号到R7寄存器,然后调用svc触发软中断

 int syscall(int number, ...);

返回值:就是系统调用号对应的内核函数的返回值,如果出错一律返回-1

number:系统调用号

...:系统调用号对应函数的参数

 

案例:利用syscall实现打印和程序退出

编译方法1:

arm-linux-gcc -otest test.c

 

编译方法2:

汇编:

arm-linux-gcc -Stest.c    //test.s

vim test.s   //不想在调用syscall,我想直接保存系统调用号,然后调用svc触发软中断

修改如下:

 .file  "test.c" ->.file "test.s"

mov     r0, #4       -> mov r7,#4         

mov     r1, #1        -> move r0, #1

movw    r2, #:lower16:.LC0  ->movw   r1, #:lower16:.LC0

movt    r2, #:upper16:.LC0 ->movt    r1, #:upper16:.LC0  

mov     r3, #14   -> mov     r2, #14             

bl      syscall     -> svc 0

 

mov     r0, #1   -》mov r7,#1          

mov     r1, #0   -> move r0,#0          

bl      syscall     -> svc 0

 

保存退出

arm-linux-gcc -otest test.s

 

编译方法3:

由于main函数在执行前,必须先执行_start

arm-linux-gcc -Stest.c //test.s 目的纯汇编代码

vim test.s

.file   "test.c" ->.file "test.s"

删除一下信息:

编译器信息处理:

  2        .eabi_attribute 27, 3

  3        .fpu neon

  4        .eabi_attribute 20, 1

  5        .eabi_attribute 21, 1

  6        .eabi_attribute 23, 3

  7        .eabi_attribute 24, 1

  8        .eabi_attribute 25, 1

  9        .eabi_attribute 26, 2

 10        .eabi_attribute 30, 6

 11        .eabi_attribute 18, 4

 

栈的处理:

 22        @ args = 0, pretend = 0, frame = 8        

 23        @ frame_needed = 1, uses_anonymous_args = 0

 24        stmfd   sp!, {fp, lr}        

 25        add     fp, sp, #4          

 26        sub     sp, sp, #8          

 27        str     r0, [fp, #-8]       

 28        str     r1, [fp, #-12] 

 

栈的处理:

 38        sub     sp, fp, #4          

 39        ldmfd   sp!, {fp, pc}

 

然后再修改一下内容:

 .file  "test.c" ->.file "test.s"

mov     r0, #4       -> mov r7,#4         

mov     r1, #1        -> move r0, #1

movw    r2, #:lower16:.LC0  ->movw   r1, #:lower16:.LC0

movt    r2, #:upper16:.LC0 ->movt    r1, #:upper16:.LC0  

mov     r3, #14   -> mov     r2, #14             

bl      syscall     -> svc 0

 

mov     r0, #1   -》mov r7,#1          

mov     r1, #0   -> move r0,#0          

bl      syscall     -> svc 0

 

arm-linux-gcc -ctest.s //编译成目标文件test.o

arm-linux-ld -otest test.o  //连接成可执行程序test

 

反汇编:

arm-linux-objdump -d test >test.dis

查看"Hello,world"只读数据段的信息

arm-linux-objdump -j .rodata -stest //查看只读数据段的

信息

 

总结:用户空间转向内核空间通过软中断!

 

案例:利用syscall实现文件的读写

实验步骤:

gcc file.c

touch a.txt

./a.out

cat a.txt

 

案例:在内核中添加新的系统调用,实现两个数加法运算

案例:在内核中添加新的系统调用,实现开关灯

实验文档:ftp/drivers/syscall_add.doc

sys_arm.c

 

//index:表示哪个灯,1或者2

//cmd:开还是关,1表示开,0表示关

voidsys_ledconfig(int index, int cmd)

{

      if (index == 1) //第一个灯

      //申请资源 

      else if (index == 2) //第二个灯

      //申请资源

      配置输出口,输出1

     

      //释放资源

}

 

问题:重新编译内核以后,重启系统,内核跑到“USB HUB”地方停住。

解决方法:

cd /opt/kernel

cpconfig_CW210_linux_V1.0 .config

make zImage

 

asmlinkage:强制要求传递参数通过栈,而不是通过寄存器,默认采用寄存器(r0~r3

 

调试内核代码使用printk打印信息!

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

总结:

1.gpio资源的申请、释放以及操作库函数

gpio是一种软件资源,需要时需申请,其他模块不可再使用,不用时,即释放,其他模块可以使用。

gpio必须配置为输出或者输入

(1)gpio_request(gpio软件编号,标签(字符串))

(2)gpio_free(gpio软件编号)

(3)gpio_direction_output(gpio软件编号,int value)

(4)gpio_direction_input(软件编号)

(5)gpio_set_value(gpio软件编号,int value) gpio为输出

(6)gpio_get_value(gpio软件编号) gpio为输出、输入都可

2.linux系统调用原理

 

 

 

 

 

 

 

 

 

 

 

 

 

#include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/eeprom.h> #include <string.h> #define delay_ms(x) _delay_ms(x) // LCD 相关引脚定义 #define LCD_RS PC0 #define LCD_RW PC1 #define LCD_E PC2 #define LCD_D4 PA4 #define LCD_D5 PA5 #define LCD_D6 PA6 #define LCD_D7 PA7 // 按键引脚定义(使用PORTD) #define SET_BUTTON PIND0 #define ADD_BUTTON PIND1 #define SUB_BUTTON PIND2 // LED 引脚定义 #define LED_PIN PB3 // EEPROM存储地址 #define EEPROM_TIME_HOUR_ADDR 0x00 #define EEPROM_TIME_MINUTE_ADDR 0x01 #define EEPROM_DATE_YEAR_ADDR 0x02 #define EEPROM_DATE_MONTH_ADDR 0x03 #define EEPROM_DATE_DAY_ADDR 0x04 // 全局变量 volatile uint8_t hour = 12; // 小时(0-23) volatile uint8_t minute = 0; // 分钟(0-59) volatile uint8_t second = 0; // 秒(0-59) volatile uint8_t year = 23; // 年份(后两位) volatile uint8_t month = 1; // 月份(1-12) volatile uint8_t day = 1; // 日期(1-31) volatile uint8_t set_mode = 0; // 0:正常显示, 1:设置小时, 2:设置分钟, 3:设置年, 4:设置月, 5:设置日 volatile uint8_t blink_state = 0; // 闪烁状态 volatile uint8_t timer1_counter = 0; // 定时器计数器 // 闹钟结构 typedef struct { uint8_t hour; uint8_t minute; uint8_t enabled; } Alarm; Alarm alarm1 = {8, 0, 0}; // 闹钟1,默认8:00,关闭 Alarm alarm2 = {12, 0, 0}; // 闹钟2,默认12:00,关闭 // LCD初始化 void LCD_Init() { // 设置数据端口为输出 DDRA |= (1<<LCD_D4) | (1<<LCD_D5) | (1<<LCD_D6) | (1<<LCD_D7); // 设置控制端口为输出 DDRC |= (1<<LCD_RS) | (1<<LCD_RW) | (1<<LCD_E); // 初始化序列 delay_ms(50); // 等待LCD上电稳定 // 4位模式初始化 LCD_Write_Command(0x33); LCD_Write_Command(0x32); LCD_Write_Command(0x28); // 4位模式,2行显示,5x8点阵 LCD_Write_Command(0x0C); // 显示开,光标关,闪烁关 LCD_Write_Command(0x06); // 增量模式,不移位 LCD_Write_Command(0x01); // 清屏 delay_ms(2); } // 向LCD发送命令 void LCD_Write_Command(unsigned char cmd) { PORTC &= ~(1<<LCD_RS); // RS=0 命令模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (cmd & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((cmd << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 向LCD发送数据 void LCD_Write_Data(unsigned char data) { PORTC |= (1<<LCD_RS); // RS=1 数据模式 PORTC &= ~(1<<LCD_RW); // RW=0 写入 // 发送高4位 PORTA = (PORTA & 0x0F) | (data & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 // 发送低4位 PORTA = (PORTA & 0x0F) | ((data << 4) & 0xF0); PORTC |= (1<<LCD_E); // E=1 delay_ms(1); PORTC &= ~(1<<LCD_E); // E=0 delay_ms(1); } // 在LCD上显示字符串 void LCD_Display_String(char *str) { while (*str) { LCD_Write_Data(*str++); } } // 设置LCD光标位置 void LCD_Set_Cursor(uint8_t row, uint8_t col) { uint8_t address; if (row == 0) { address = 0x80 + col; } else { address = 0xC0 + col; } LCD_Write_Command(address); } // 显示时间 void Display_Time() { char buffer[16]; // 第一行显示时间 LCD_Set_Cursor(0, 4); sprintf(buffer, "%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); // 第二行显示日期 LCD_Set_Cursor(1, 4); sprintf(buffer, "20%02d-%02d-%02d", year, month, day); LCD_Display_String(buffer); } // 显示设置模式 void Display_Set_Mode() { char buffer[16]; LCD_Set_Cursor(0, 0); LCD_Display_String("Set:"); switch(set_mode) { case 1: // 设置小时 if(blink_state) { sprintf(buffer, "Hour: %02d", hour); } else { sprintf(buffer, "Hour: "); } break; case 2: // 设置分钟 if(blink_state) { sprintf(buffer, "Minute:%02d", minute); } else { sprintf(buffer, "Minute: "); } break; case 3: // 设置年 if(blink_state) { sprintf(buffer, "Year: 20%02d", year); } else { sprintf(buffer, "Year: "); } break; case 4: // 设置月 if(blink_state) { sprintf(buffer, "Month: %02d", month); } else { sprintf(buffer, "Month: "); } break; case 5: // 设置日 if(blink_state) { sprintf(buffer, "Day: %02d", day); } else { sprintf(buffer, "Day: "); } break; default: sprintf(buffer, "Normal Mode "); } LCD_Set_Cursor(0, 5); LCD_Display_String(buffer); // 仍然显示时间日期 LCD_Set_Cursor(1, 0); sprintf(buffer, "Time:%02d:%02d:%02d", hour, minute, second); LCD_Display_String(buffer); } // 从EEPROM加载时间日期 void Load_Time_From_EEPROM() { hour = eeprom_read_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR); minute = eeprom_read_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR); year = eeprom_read_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR); month = eeprom_read_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR); day = eeprom_read_byte((uint8_t*)EEPROM_DATE_DAY_ADDR); // 检查读取的值是否有效 if(hour > 23) hour = 0; if(minute > 59) minute = 0; if(year > 99) year = 23; if(month == 0 || month > 12) month = 1; if(day == 0 || day > 31) day = 1; } // 保存时间日期到EEPROM void Save_Time_To_EEPROM() { eeprom_update_byte((uint8_t*)EEPROM_TIME_HOUR_ADDR, hour); eeprom_update_byte((uint8_t*)EEPROM_TIME_MINUTE_ADDR, minute); eeprom_update_byte((uint8_t*)EEPROM_DATE_YEAR_ADDR, year); eeprom_update_byte((uint8_t*)EEPROM_DATE_MONTH_ADDR, month); eeprom_update_byte((uint8_t*)EEPROM_DATE_DAY_ADDR, day); } // 初始化定时器1 (1秒中断) void Timer1_Init() { // 设置定时器1为CTC模式 TCCR1B |= (1 << WGM12); // 设置预分频为1024 TCCR1B |= (1 << CS12) | (1 << CS10); // 设置比较值 (16MHz/1024 = 15625 ticks/sec, 15625 ticks = 1秒) OCR1A = 15625; // 启用比较匹配中断 TIMSK |= (1 << OCIE1A); } // 初始化按键引脚 void Buttons_Init() { // 设置按键引脚为输入,启用上拉电阻 DDRD &= ~((1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON)); PORTD |= (1<<SET_BUTTON) | (1<<ADD_BUTTON) | (1<<SUB_BUTTON); } // 按键扫描 void Key_Scan() { static uint8_t last_set_state = 1; static uint8_t last_add_state = 1; static uint8_t last_sub_state = 1; uint8_t current_set_state = PIND & (1<<SET_BUTTON); uint8_t current_add_state = PIND & (1<<ADD_BUTTON); uint8_t current_sub_state = PIND & (1<<SUB_BUTTON); // 检测设置按键按下 if(last_set_state && !current_set_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SET_BUTTON))) { set_mode++; if(set_mode > 5) set_mode = 0; if(set_mode == 0) { // 退出设置模式,保存时间到EEPROM Save_Time_To_EEPROM(); } } } // 只在设置模式下检测加减按键 if(set_mode > 0) { // 检测增加按键按下 if(last_add_state && !current_add_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<ADD_BUTTON))) { switch(set_mode) { case 1: // 增加小时 hour = (hour + 1) % 24; break; case 2: // 增加分钟 minute = (minute + 1) % 60; break; case 3: // 增加年 year = (year + 1) % 100; break; case 4: // 增加月 month = (month % 12) + 1; break; case 5: // 增加日 day = (day % 31) + 1; break; } } } // 检测减少按键按下 if(last_sub_state && !current_sub_state) { _delay_ms(20); // 消抖 if(!(PIND & (1<<SUB_BUTTON))) { switch(set_mode) { case 1: // 减少小时 hour = (hour == 0) ? 23 : hour - 1; break; case 2: // 减少分钟 minute = (minute == 0) ? 59 : minute - 1; break; case 3: // 减少年 year = (year == 0) ? 99 : year - 1; break; case 4: // 减少月 month = (month == 1) ? 12 : month - 1; break; case 5: // 减少日 day = (day == 1) ? 31 : day - 1; break; } } } } last_set_state = current_set_state; last_add_state = current_add_state; last_sub_state = current_sub_state; } // 检查闹钟 void Check_Alarms() { static uint8_t alarm1_triggered = 0; static uint8_t alarm2_triggered = 0; // 检查闹钟1 if(alarm1.enabled && !alarm1_triggered && hour == alarm1.hour && minute == alarm1.minute && second == 0) { alarm1_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 检查闹钟2 if(alarm2.enabled && !alarm2_triggered && hour == alarm2.hour && minute == alarm2.minute && second == 0) { alarm2_triggered = 1; PORTB |= (1<<LED_PIN); // 打开LED } // 每分钟重置闹钟触发标志 if(second == 0) { alarm1_triggered = 0; alarm2_triggered = 0; } // 如果LED亮起,检测按键关闭 if(PORTB & (1<<LED_PIN)) { if(!(PIND & (1<<SET_BUTTON)) || !(PIND & (1<<ADD_BUTTON)) || !(PIND & (1<<SUB_BUTTON))) { PORTB &= ~(1<<LED_PIN); // 关闭LED } } } // 定时器1比较匹配中断服务程序 ISR(TIMER1_COMPA_vect) { // 更新时间 second++; if(second >= 60) { second = 0; minute++; if(minute >= 60) { minute = 0; hour++; if(hour >= 24) { hour = 0; // 日期增加逻辑 day++; uint8_t max_day = 31; // 处理不同月份的天数 if(month == 4 || month == 6 || month == 9 || month == 11) { max_day = 30; } else if(month == 2) { // 简单处理2月天数(不考虑闰年) max_day = 28; } if(day > max_day) { day = 1; month++; if(month > 12) { month = 1; year++; if(year > 99) year = 0; } } } } } // 更新闪烁状态 (0.5秒周期) timer1_counter++; if(timer1_counter >= 5) { // 10次 = 1秒 (0.1秒中断) timer1_counter = 0; blink_state = !blink_state; } // 检查闹钟 Check_Alarms(); } int main(void) { // 初始化端口 DDRB |= (1<<LED_PIN); // LED引脚为输出 PORTB &= ~(1<<LED_PIN); // 初始关闭LED // 初始化按键 Buttons_Init(); // 初始化LCD LCD_Init(); // 从EEPROM加载时间 Load_Time_From_EEPROM(); // 初始化定时器 Timer1_Init(); // 启用全局中断 sei(); // 清屏并显示欢迎信息 LCD_Write_Command(0x01); LCD_Set_Cursor(0, 3); LCD_Display_String("AVR Clock"); LCD_Set_Cursor(1, 2); LCD_Display_String("Initializing..."); delay_ms(1000); LCD_Write_Command(0x01); while(1) { // 扫描按键 Key_Scan(); // 更新显示 if(set_mode == 0) { Display_Time(); } else { Display_Set_Mode(); } // 短暂延迟 delay_ms(100); } return 0; }使用的atmega128单片机,不改动引脚的使用
最新发布
05-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值