drivers_day02

本文介绍嵌入式Linux环境下内核模块的开发流程,包括模块的编译、加载及卸载方法,以及内核打印函数printk的使用技巧。

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

回顾:

1.回顾嵌入式linux系统相关内容

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

用户空间:

      CPU的工作模式为:USR(用户)模式,也是非特权模式

      运行:应用程序,C库等

      地址空间:4G虚拟地址空间,占前3G

      地址范围:0x00000000~0xBFFFFFFF

      不能直接访问硬件资源,不能访问内核空间地址

内核空间:

      CPU工作模式:SVC(管理)模式,也是特权模式

      设备驱动,网络协议栈,文件系统,进程管理,内存管理,平台相关,系统调用      (open,close,read,write,mmap,fork...)

      地址范围:0xC0000000 ~ 0xFFFFFFFF

      能够访问任何资源   

3.用户空间和内核空间进行通信必须通过系统调用~!

4.用户进程一旦陷入内核空间运行,内核会给进程分配一个内核栈(8K)?

5.了解和掌握linux内核的配置编译

----------------------------------------------------------------------------------------------------------------------------------------

内核代码:helloworld.c

静态编译:将helloworld.c和zImage编译在一起

动态编译,模块化编译:将helloworld.c和zImage分开编译,将helloworld.c编译成对应的二进制文件,默认以ko结尾,例如helloworld.ko

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

Day02

内核模块的动态编译:

内核编译借助KconfigMakefile,Kconfig和Makefile的选择关键看helloworld.c放置在哪个目录下,例如将helloworld.c放置在内核源码的drivers/char/

修改Kconfig:

vimdrivers/char/Kconfig,添加:

configHELLOWORLD

      tristate "hello,world"

      --help---

           "this is my first kernel module"

注意:config HELLOWORLD最终生成的给Makefile使用的配置项:CONFIG_HELLOWORLD(才是Kconfig和Makefile连接的桥梁)

修改Makefile文件:

obj-$(COFNIG_HELLOWORLD)+= helloworld.o

 

每当make menuconfig进行配置时,如果选择为*

obj-y +=helloworld.o //静态编译

如果选择为M

obj-m +=helloworld.o //动态编译-》helloworld.ko

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

一,内核编程

1.入口函数

      如果需要驱动有一个入口函数,那么通过内核提供的宏函数进行修饰,告知内核驱动的入口函数

      module_init(hellokernel_init);

      入口函数的定义要求:

      int hellokernel_init(void)

      返回值:int型,如果入口函数正确执行,给内核返回0,否则返回负值;

入口函数正确执行了,内核模块才能加载到内核,否则加载失败!

      参数:void

      函数功能向内核注册操作硬件的方法或者申请资源,例如分配内存

      注意:一个驱动(.c)只能有一个入口函数

      必须通过module_init来告诉内核这是入口函数!

      什么时候被内核执行?

      每当内核初始化(内核模块采用静态编译进zImage)或者动态加载驱动模块到内核时,内核会执行入口函数!

2.出口函数

      如果驱动有必要添加一个出口函数,那么通过内核提供的宏函数进行修饰,告知内核驱动的出口函数

      module_exit(hellokernel_exit);

      出口函数的定义:

      void hellokernel_exit(void)

      参数,返回值:void

      函数功能:一般和入口函数相对应,做些释放操作硬件方法和释放资源的工作,例如释放内存

      注意:一个驱动(.c)只有一个出口函数

      什么时候被内核执行?

      系统复位或者动态将模块从内核卸载时,内核调用出口函数。

 

案例:编写第一个内核模块代码helloworld.c

驱动存放目录:/opt/drivers/day01,/opt/drivers/day02...

mkdir/opt/drivers/day01/1.0 -p

cd/opt/drivers/day01/1.0

vim helloworld.c

 

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

3.内核模块代码编译(采用模块化编译)

明确:内核模块代码的编译必须使用内核源码,不能使用C库!

编写Makefile实现将helloworld.c的目录,源码和内核源码进行关联,就是告诉内核源码,还有一个内核模块的目录,不是在内核源码中,而是在/opt/drivers/day01

vim Makefile 内容如下:

obj-m +=helloworld.o

KDIR=/opt/kernel

all:

(TAB键)make -C $(KDIR)SUBDIRS=$(PWD) modules

#说明:-C表示指定到某个目录下,SUBDIRS表示子目录 modules表示模块化编译-》helloworld.ko

clean:

(TAB键) rm -fr .*.cmd  *.o *.mod.c *.ko  .tmp_versions module*  Module*

 

编译:

make //结果生成一个目标文件helloworld.ko

filehelloworld.ko //查看是否是针对ARM架构

 

注意:编译模块之前一定要先编译内核源码,还要在内核源码中进行对内核源码进行模块编译

实施步骤:

1.cd /opt/kernel

2.cpconfig_cw210...V1.0 .config

3.makemenuconfig //3个检查

4.make zImage

5.make modules//把内核源码中但凡选择为M的代码进行模块化编译

总结:每当进行模块化编译内核代码是,最后前期做好以上5步骤,只需做一次即可!

 

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

内核模块的使用(helloworld.ko):

linux系统提供了模块操作的相关命令:

insmod:模块加载命令,例如insmod helloworld.ko,就是将模块helloworld.ko动态加载到内核中,如果模块有入口函数,内核此时执行模块的入口函数,如果没有入口函数,没关系,不影响模块的加载!

lsmod:查看当前运行中的内核已经加载的模块,例如,lsmod列出当前已经加载了哪些模块

rmmod:从内核中动态删除指定的模块,例如rmmod helloworld,就是将模块helloworld从内核动态删除掉,如果模块有出口函数,内核执行此模块的出口函数。

modinfo:查看模块本身的信息,例如modinfo helloworld.ko

 

实验步骤:

1.cp/opt/drivers/day01/1.0/hellooworld.ko /opt/rootfs

在开发板上:

2.lsmod查看

2.加载insmod helloworld.ko

3.查看lsmod

4.卸载rmmod helloworld

5.modinfohelloworld.ko

问题:rmmod卸载模块失败,提示:

rmmod:chdir(/lib/modules):Nosuch file or dir...

问题的原因是制作rootfs时,busybox配置时,模块操作的命令都是选择的精简版本,最好使用完成版本,如果使用精简版本的模块命令,解决以上的问题的方法:

mkdir/lib/modules/2.6.35.7-Concenwit -p

 

如何将busybox配置添加完整版的模块操作命令:

cdbusybox-1.19.4

make menuconfig

 Linux Module Utilities  --->

       [*] Simplified modutils     //精简版本,如果选用完整版本,这个选项去掉!然后将一下信息全部选中!

   [*]   insmod                                                                              [*]   rmmod                                                                       

    [*]  lsmod                                                                       

    [*]  Pretty output                                                             

    [*]  modprobe                                                                         [*]   Blacklist support                                                         

    [*]  depmod

make

make install

cp _install/*/opt/rootfs -frd

        

问题:

~ # modinfohelloworld.ko

modinfo: can'topen '/lib/modules/2.6.35.7-Concenwit/modules.dep': No such file or directory

后续讲解!

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

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

1.给内核模块必须添加许可声明

      MODULE_LICENSE(GPL);//告诉内核此模块遵循GPL协议

      注意:以后写任何一个.c内核代码,都必须添加这么一句话!

2.给内核模块添加可选信息:

      MODULE_AUTHOR(“tarena<40902560@qq.com>”);

 

      MODULE_DESCRIPTION(“Hello Kernel module”);

 

      MODULE_VERSION(“V1.0.0”);

3.查看以上模块信息通过modinfo来查看

 

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

内核模块参数声明:

作用:像应用程序命令行传参一样,给程序传递参数信息./a.out 100 200.内核如果也需要给程序传递参数,需要使用内核模块参数声明来解决。

如果要对内核模块的某一个变量进行修改,必须要声明:

module_param(name,type,perm);

name:要修改的变量名

type:变量的数据类型,没有浮点数

perm:对变量的访问权限,如果权限非0,每当加载完模块以后,会在/sys/module(虚拟文件系统的目录,存在于内存中,不存在nfs的主机上或者flash上,并且与其他的根文件系统一样,在内存中还有一份)目录下生成一个跟模块名同名的目录,在这个目录下有一个parameters,在这个目录下会生成一个跟变量名同名的文件,以后通过修改这个文件,来修改这个变量的值;如果权限为0,不会有同名的文件生成,只能在insmod模块时才能对这个变量进行赋值!

实验步骤:

1.不传参

insmodhelloworld.ko

lsmod

rmmod helloworld

2.传参

insmod helloworld.ko irq=100pstr=tarena

lsmod

rmmod helloworld

3.修改文件来修改变量的内容

insmodhelloworld.ko irq=100 pstr=tarena

lsmod

ls /sys/module/helloworld/parameters/

cat  /sys/module/helloworld/parameters/irq //读文件

echo  2000 >  /sys/module/helloworld/parameters/irq //修改文件内容

rmmod helloworld//查看irq是否修改为2000

案例:把pstr的权限也给0664

 

数组的模块参数声明:

module_param_array(name,type,nump,perm);

作用:对数组进行模块参数声明,一旦声明,以后可以进行对数组传递参数

name:数组名

type:数据元素的数据类型

nump:记录有效的数组元素个数的指针

perm:访问权限

进行模块定义、实现的时候,只需定义一个变量,将它的地址给module_param_array的第三个参数即可!然后内核会自动根据有效的数组元素的个数给其赋值!

实验步骤:

insmodhelloworld.ko fish=10,20,30,40,50 //nr_fish=5

cat/sys/module/helloworld/parameters/fish

echo1,2,3,4,5,6,7,8 > /sys/module/helloworld/parameters/fish

rmmod helloworld

 

总结:虽然在进行模块参数声明的时候,如果给定了权限,那么就会在指定的目录下生成一个跟变量名同名的文件,修改文件即可对变量进行修改,这种方式虽然比较方便,灵活,但是这个文件存在于内存中,如果驱动有大量的这样代码,会造成内存的大量使用!如果没有需要在模块运行期间改变变量内容的需求,权限一律给0!如果权限为0,只能在insmod时修改!

 

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

内核符号导出:

作用:将函数或者变量进行符号导出,让其他模块能够访问使用。

EXPORT_SYMBOL(函数名或者变量名);

EXPORT_SYMBOL_GPL(函数名或者变量名)

前者导出的符号能够被任何模块使用,但是后者导出的符号只能被遵循GPL协议的模块使用!

 

案例:helloworld.c调用test_module.c的某个函数

调试宏:

__FUNCTION__,__func__,%s

__LINE__,%d

__FILE__,%s

__DATE__,%s

__TIME__,%s

 

内核多文件编译:

Makefile:

分别编译:

obj-m += A.o B.o

编译在一起:

obj-m += C.o //不能是A.o或者B.o

C-objs = A.o B.o

 

多文件模块加载:一定首先先加载依赖模块,例如A.ko调用B.ko的某个函数,那么A依赖B。先加载insmod B.ko,后加载insmod A.ko.卸载时,先卸载A,最后卸载依赖模块B。

 

总结:写任何.c文件,都要加MODULE_LICENSE("GPL");

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

如果模块之间的依赖过于复杂,可以利用modprobe命令来加载或者卸载模块。例如.A.ko依赖B.ko,每当modprobe A.ko时,此命令根据modules.dep,先帮你加载B.ko,后加载A.ko.

 

modprobe命令使用步骤:

1.进入内核源码 cd /opt/kernel

   make modules

 

2.回到驱动模块所在目录/opt/drivers/day02/3.0

2.修改Makefile,添加如下信息:

install:

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

#modules_install表示安装模块,主要是产生modules.dep依赖关系文件,然后将.ko拷贝到extra目录。

#INSTALL_MOD_PATH表示安装到那个目录下

#一旦执行make install安装模块时,最后在/opt目录生成一个lib目录,然后将lib目录的内容拷贝到/opt/rootfs/lib下

 

3.make  //编译模块

4.make install//安装模块

5. cp /opt/lib/*/opt/rootfs/lib/ -frd

6.在开发板上进行加载:

   modprobe helloworld.ko / modprobe helloworld

   说明:modprobe默认到/lib/modules/2.6..../下找依赖文件modules.dep,根据依赖文件,决定先加载哪个模块!模块在/lib/modules/2.6..../extra

 

   卸载:

   modprobe -r helloworld

问题:

~ # modinfohelloworld.ko

modinfo: can'topen '/lib/modules/2.6.35.7-Concenwit/modules.dep': No such file or directory

通过以上步骤也能够解决掉!

 

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

二:内核打印函数printk用法

1.printk能够指定输出打印级别(0~7)

2.printk打印输出信息要依赖默认的输出级别,printk函数在使用时,指定的输出级别如果小于默认的输出级别,信息一律打印,否则(大于等于)不打印!

3.设置默认的输出级别的方法:

方法1:通过修改配置文件

cat /proc/sys/kernel/printk

7(关注第一个,默认的输出级别为7)       4      1       7

echo 8 >/proc/sys/kernel/printk

缺点:无法设置内核启动时的打印信息

 

方法2:通过修改ubootbootargs来实现,这种设置方法能够解决内核启动时的打印输出

setenv bootargsroot=/dev/nfs nfsroot=... quiet//设置默认的输出级别为4

boot //启动或者res(复位)

u-boot下,boot为直接启动内核的命令,res复位从u-boot开始启动!

在内核下,reboot为重启系统命令,直接从u-boot开始启动,相当于复位!

(1)setenv bootargsroot=/dev/nfs nfsroot=... debug//设置的默认输出级别为10

    save然后boot启动内核

setenv bootargsroot=/dev/nfs nfsroot=... loglevel=数字

如果不加这三个条件,则默认的输出级别为7,都可通过查看/proc/sys/kernel/printk来检查

Boot

案例:

static intprintall_init(void)

{

    printk("<0>" "level 0\n");

    printk("<1>" "level 1\n");

    printk("<2>" "level2\n");

    printk("<3>" "level3\n");

    printk("<4>" "level4\n");

    printk("<5>" "level5\n");

    printk("<6>" "level6\n");

    printk("<7>" "level7\n");

    return 0;

}

输出级别与格式字符串之间,没有逗号,空格或者直接相连即可!

未指定优先级的默认级别定义在内核根目录下的/kernel/printk.c下,

本系统中在/opt/kernel/kernel/printk.c下,

#defineDEFAULT_MESSAGE_LOGLEVEL 4

总结:产品最终发布时,打印信息一律屏蔽,使用loglevle=0!产品软件研发阶段,使用debug来查看程序的运行状态

quiet 为4,debug为10,loglevel=数字(自己设置输出级别)

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

总结:

1.内核打印函数printk

默认的输出级别的设置,两种方法:1)内核启动以后设置 /proc/sys/kernel/printk2)设置u-boot的环境变量bootargs 添加quiet/debug/loglevel=数字

未指定输出级别的默认级别的设置在/kernel/printk.c下的宏 #defineDEFAULT_MESSAGE_LOGLEVEL   4

2.

 

 

 

 

 

 

 

 

 

 

 

#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、付费专栏及课程。

余额充值