drivers_day05

本文深入探讨了Linux内核字符设备驱动的实现细节,包括设备驱动分类、设备文件概念、设备号管理、重要数据结构解析及操作接口介绍,旨在帮助读者理解字符设备的工作原理。

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

回顾:

linux内核字符设备驱动实现

1.linux内核设备驱动分类

字符设备:字节流,串口,LED,按键,蜂鸣器,ADC,声卡,显卡,LCD液晶屏,触摸屏,各类传感器,GPS,GPRS,蓝牙

块设备:512字节,硬盘,光盘,SD卡,TF卡,nandflash(SLC,MLC,TLC),emmc,U盘

网络设备:网卡,配合网络协议栈

2.设备文件

“一切皆文件”;

硬件设备在用户空间以设备文件的形式存在;

在/dev/目录下,设备文件存在于内存中;

用户访问设备就是通过访问设备文件来进行,同时要配合系统调用函数(open,close,read,write...);

mknod /dev/xxx c主设备号 次设备号 ,用于创建设备文件!

 

3.设备号

主设备号:应用程序根据主设备号能够找到自己对应的字符设备驱动。一个设备驱动只有唯一的一个主设备号。

次设备号:应用程序根据主设备号找到驱动以后,如果驱动管理多个同类的硬件设备,驱动再通过次设备号来分区具体操作哪个硬件设备个体!

数据类型:dev_t (unsigned int)

typedef unsigendint dev_t;

高12:主

低20:次

MAJOR

MINOR

MKDEV

设备号对于内核来说是一种宝贵的资源,所以驱动要使用某一个设备号的时候一定要先向内核去申请(类似申请内存一样),当然设备号驱动不再使用时,一定要归还给内核。

申请方法:

1.静态申请

   register_chrdev_region(dev_t dev, int count,name);

2.动态申请

  alloc_chrdev_region(dev_t *dev, 0, count, name);

4.字符设备涉及的4个重要数据结构

struct cdev:

描述字符设备;

重要字段:

      dev_t dev; //存放设备号

      int count; //存放设备的个数

      struct file_operations *ops; //通过这个指针能够给字符设备附加一些操作硬件的方法

如何使用:

分配字符设备对象

struct cdevled_cdev;

初始化对象:dev,count,ops

cdev_init(分配字符设备对象指针,硬件操作方法的指针);

ops指针最终指向驱动分配初始化的操作集合led_fops;

注册字符设备对象到内核中

cdev_add(分配字符设备对象指针,设备号,设备个数);

其实就是将分配字符设备对象指针以设备号为索引添加到内核的cdev数组中;

字符设备驱动一旦注册成功,静静等待着用户来访问!

 

structfile_operations:

就是包含了一堆的函数指针,这些函数指针指向驱动的某个函数

,并且把这个结构体指针赋值给cdev对象的.ops.在通过cdev将这些硬件的方法给用户使用:

structfile_operations led_fops = {

      .owner = THIS_MODULE,

      .open = led_open, //打开设备

      .release = led_close, //关闭设备

      .read = led_read, //读设备

      .write = led_write //写设备

};

app:open->软中断->sys_open->led_open

app:close->软中断->sys_close->led_close

app:read->软中断->sys_read->led_read

app:write->软中断->sys_write->led_write

...

本质上这个结构体提供的方法是用户来使用!

structfile_operations不能直接注册到内核中,要通过cdev间接的注册到内核中!

 

struct inode:

用来描述一个文件(所有文件)的物理信息,文件存在,内核就会分配一个inode对象来描述这个文件的物理信息,文件一旦销毁,内核也会将对应的inode对象销毁!

每当用户mknod创建设备文件时,内核做了哪些事情?

1.内核分配inode对象

2.内核根据mknod命令指定的主设备号,次设备号,初始化inode对象的一个成员i_rdev(指定的设备号),驱动通过inode来获取设备号,从而获取次设备号,用于驱动来分区具体操作的哪个设备个体!

3.内核以设备号为索引,在内核的cdev数组中找到对应的字符设备对象led_cdev指针,然后将这个指针赋值给inode对象的一个成员i_cdev(inode和字符设备驱动进行关联)

 

struct file:

用来描述一个文件被成功打开以后的状态信息,文件被打开,内核就会创建一个file对象来描述文件被打开以后的状态,文件被关闭,内核也会销毁对应的file对象!

一个文件只有一个inode,但是可以有多个file;

应用程序访问设备永远先打开(open)设备:

1.app:in fd =open("/dev/myled");

2.C库的open函数实现,保存open的系统调用号到R7中,调用SVC触发软中断

3.CPU跳转到内核的异常向量表的入口地址

4.根据R7中的系统调用号,在系统调用表找对应的函数sys_open;

5.sys_open内核会做:

   5.1 分配file对象

   5.2 初始化file对象的一个成员f_op,通过已知的cdev对象,从这个对象的ops中获取硬件操作集合(&led_fops)指针,然后把这个指针赋值给f_op,这样file对象就有对应的硬件操作方法!其他的系统调函数至此再跟cdev对象没有任何关系,只跟file的f_op有关系。

   5.3 并且将设备文件描述符fd和file对象进行关联!

   5.4 判断file->f_op是否有open函数,如果有,调用,如果没有,给用户永远返回成功!

6.用户读设备:

   1.app:read(fd, buf, size);

   2.sys_read:

      根据fd获取关联的file

      file->f_op->read(...) =&led_fops->read = led_read.

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

案例:在昨天的驱动代码中,在led_open和led_close函数中,通过inode指针来获取设备号,并打印出来!

提示:

static intled_open(struct inode *inode, struct file *file)

{

      int major = MAJOR(inode->i_rdev);

      int minor = MINOR(inode->i_rdev);

}

 

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

structfile_operations相关操作接口:

对设备的打开,关闭操作:

int(*open)(struct inode *inode, struct file *file);

int(*release)(struct inode *inode, struct file *file);

以上两个函数,底层驱动可以不用实现,如果不做实现,用户空间的open永远返回成功!

 

对设备的读写操作:

ssize_t(*read)(struct file *file, char __user *buf,

           size_t count, loff_t *ppos);

ssize_t(*write)(struct file *file, char __user *buf,

           size_t count, loff_t *ppos);

前者表示用户读设备

后者表示用户写设备

file:sys_open创建的file对象,对应用户空间read,write函数的第一个参数fd

buf:但凡内核用__user修饰的指针变量,那么这个指针变量指向的地址永远是用户空间(0x00000000~0xBFFFFFFF).所以这个buf指向用户空间的缓冲区(内存)。在内核空间不能直接访问操作这个buf(*buf = 1,这是错误的!),需要利用内核提供的内存拷贝函数来实现。buf是和用户空间read,write的第二个参数进行关联。

count:请求读写的字节数,和用户空间read,write的第三个参数关联。

ppos:记录上一次读写位置,如果想获取上一次的读写位置:

loff_t pos =*ppos;如果操作完毕以后,有必要更新位置:

*ppos = 新位置;

返回值:实际读写的字节数

 

使用注意事项:

1.如果用户调用read,write来读写设备,驱动程序必须给定read,write的函数实现;

2.read,write的第二个参数buf指向用户空间,不能在内核空间直接访问操作,必须利用内核提供的内存拷贝函数实现内核空间和用户空间的数据传递!

 

内核提供的内存拷贝函数:

注意:这些内存拷贝函数不代表读,写行为,仅仅代表数据流的走向!

内核缓冲区(源) -------->用户缓冲区(目标)

copy_to_user(void *to, void *from, unsigned long n)

to:目标,它是地址

from:源,它是地址

n:拷贝字节数

这个函数能够对任何数据类型进行拷贝,包括结构体!

 

put_user(data,ptr)

data:内核变量,而不是地址;不能是结构体;

ptr:用户缓冲区首地址,它是地址

这个函数在使用的时候,ptr的数据类型一定要和data的数据类型保持一致!例如:

int data = 100;//驱动定义的变量

int *p = (int*)buf; //把buf从char *转换成int *

put_user(data,p); //将内核的变量data拷贝到用户空间的buf中,拷贝4个字节

 

用户缓冲区(源)  -------->内核缓冲区(目的)

copy_from_user(void *to, void *from, n)

to:目标,它是地址

from:源,它是地址

n:拷贝字节数

这个函数能够对任何数据类型进行拷贝,包括结构体!

 

get_user(data,ptr)

data:内核变量,而不是地址;不能是结构体;

ptr:用户缓冲区首地址,它是地址

这个函数在使用的时候,ptr的数据类型一定要和data的数据类型保持一致!例如:

int data; //驱动定义的变量

int *p = (int*)buf; //把buf从char *转换成int *

get_user(data,p); //从用户空间的buf中拷贝4个字节到内核的data变量中

 

案例:利用write来实现开关所有的灯,用户写1,开灯,用户写0,关灯,并且用户能够获取灯的状态!

分析:

read/write->structfile_operations->cdev->分配,初始化,注册->GPIO资源的申请->配置->释放

 

应用程序:

写设备

int fd;

int ucmd; //用户缓冲区

 

fd =open("/dev/myled", O_RDWR);

ucmd = 1; //向内核驱动写1

write(fd,&ucmd, sizeof(ucmd));

驱动:

led_write(file,buf, count, ppos);

参数对应关系:

fd->file

&ucmd->buf

sizeof(ucmd)->count

ppos->驱动维护操作的文件读写位置

 

应用程序:

读设备

int ustate;

read(fd,&ustate, sizeof(ustate)); //从驱动中获取数据,并将数据信息赋值给用户的ustate变量

驱动:

led_read(file,buf, count, ppos);

参数对应关系:

fd->file

&ustate->buf

sizeof(ustate)->count;

ppos->内核记录文件读位置

 

实验步骤:

PC:

make

arm-linux-gcc -oled_test led_test.c

cp led_drv.ko/opt/rootfs

cp led_test/opt/rootfs

 

ARM:

insmodled_drv.ko

cat/proc/devices //查看主设备号

mknod /dev/myledc 250 0

./led_test on

./led_test off

 

案例:升级驱动,能够让用户指定操作某一个灯的开关

./led_test on 1

./led_test on 2

./led_test off 1

./led_test off 2

分析:

之前的应用程序传递给驱动的操作数据信息只有一个(开关命令);如果要满足现在的要求,应用程序需要传递2个数据信息(一个是开关命令,一个是灯的编号)

struct led_cmd {

      int cmd; //开关命令,1开,0关

      int index;//灯编号,1,2

};

 

注意:创建设备文件名不能为/dev/led(官方LED驱动使用此设备文件)

 

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

向设备发送命令接口ioctl

structfile_operations {

int (*ioctl)(struct inode *inode, struct file *file,

       unsigned int cmd, unsigned long arg);

};

 

应用程序ioctl系统调用函数说明:

头文件:#include <sys/ioctl.h>

函数原型:

int ioctl(int d, int request, ...); //可变参函数

函数功能:能够利用此函数实现与设备的交互(读写)

参数说明:

d:设备文件描述符fd,与驱动的file关联,file再跟inode关联。

request:向设备发送的命令,命令最终会赋值给驱动ioctl的第三个参数cmd

...:还可以跟一个参数,这个参数一般指定为用户空间的缓冲区首地址,也就是所谓ioctl的第三个参数,这个参数是一个地址,它对应的就是驱动ioctl函数的第四个参数。

 

说明:linux内核用unsigned long这种数据类型是一定道理的,对于编译器来说,unsignedlong它的长度永远为4个字节,所以unsigned long变量可以存放任何值,包括地址!但是unsigned int 的数据类型的长度有可能为2个字节,也有可能是4个字节,一般默认是4个字节。

 

ioctl用户空间使用范例:

#define LED_ON      0x100001 //开灯命令

#define LED_OFF     0x100002 //关灯命令

 

ioctl用两个参数:

ioctl(fd,LED_ON);   //向设备发送开灯命令

ioctl(fd,LED_OFF);  //向设备发送关灯命令

 

ioctl用三个参数:

int index = 1;

ioctl(fd, LED_ON, &index); //向设备发送命令,并且传递给设备驱动一个用户空间缓冲区首地址(&index)

 

驱动ioctl函数:

app:ioctl->sys_ioctl->led_ioctl:

int(*ioctl)(struct inode *inode, struct file *file,

       unsigned int cmd, unsigned long arg);

函数功能:接收用户发来的命令,并且响应处理命令

参数:

inode:文件节点指针

file:文件指针

以上两个结构体指针跟用户空间的fd关联;

cmd:保存用户ioctl发来的命令,也对应用户空间ioctl的第二个参数。

arg:它的数据类型是unsigned long型,表明arg能够存放任何值,包括地址,所以arg存放ioctl发来的用户空间缓冲区的首地址,在驱动使用arg时,一定要进行数据类型的转换,并且驱动不能直接访问操作arg(类似read,write的buf),如果驱动要从arg用户缓冲区中获取数据或者写入数据,必须利用内核提供的4个内存拷贝函数!

驱动:

led_ioctl(...) {

      int index;

      copy_from_user(&index, (int *)arg, 4);

}

 

案例:利用ioctl实现灯的开关

应用程序ioctl(fd, ucmd, &index);

驱动ioctl(inode, file, kcmd, arg);

对应关系:

fd->inode,file

ucmd->kcmd

&index->arg:内核不能直接访问arg,因为arg存放的是用户缓冲区的首地址(&index),如果要对用户空间缓冲区进行访问,必须利用内核提供的4个内存拷贝函数。如果利用内存拷贝函数,要注意对arg进行数据类型的转换!

 

案例:利用write或者ioctl实现控制蜂鸣器!

 

 

 

 

 

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

余额充值