Linux开发长文
字符数量统计: 67091
瑞利判据(Rayleigh Criterion)是光学系统中用于评估光斑质量的一种方法。它用于确定两个光斑之间的最小可分辨距离,即两个光斑之间的最小距离,使得它们仍然可以被分辨为两个独立的点。
瑞利判据的公式为:
其中,
CD:最小可分辨距离
k1:瑞利常数,通常取值为1.22
λ:光波长
NA:数值孔径
瑞利判据可以用于评估光学系统的分辨率,以及确定光学系统的最小分辨距离。
高数值孔径EUV(Extreme Ultraviolet)光刻技术,是半导体制造中的一种光刻技术,用于制造微米级及更小尺寸的半导体芯片。
High NA EUV光刻技术使用极紫外光(EUV光)来照射半导体晶圆,以形成精细的电路图案。EUV光具有非常短的波长,可以穿透更深的层,从而实现更小的特征尺寸。这种技术对于制造高性能、低功耗的半导体芯片至关重要。
High NA EUV光刻技术需要非常高的数值孔径(NA),通常在0.5到1.0之间。数值孔径是指光束的会聚能力,它决定了光束在物体表面上的聚焦程度。数值孔径越高,光束在物体表面上的聚焦程度越高,从而可以实现更小的特征尺寸。
High NA EUV光刻技术是半导体制造领域的一项重要技术,它对于推动半导体行业的发展具有重要意义。然而,这种技术也面临着一些挑战,例如对光刻机的精度和稳定性的要求非常高,以及EUV光的产生和传输需要特殊的设备和工艺。
MOSFET,一种平面FET,通过给栅极和源极施加不同电压控制漏极的电流大小。MOSFET在微米级和20nm以上工艺具有不错的性能,但沟道长度在20nm以下时由于漏电电流问题,不再适用。
FINFET,一种3D结构,外形类似于鳍片,长期统治高端半导体工艺,但在3nm以下漏电问题也开始显著。
GAAFET,环绕式栅极场效晶体管。英特尔18A技术采用此工艺。
STM32F103ZET6是一款高性能的32位ARM Cortex-M3微控制器,由意法半导体(STMicroelectronics)生产。它具有丰富的外设和强大的处理能力,适用于各种嵌入式应用。
2.3. CH554G增强型8051单片机LED灯闪烁逆向分析Arduino示例代码
Arduino并不原生支持CH55x系列单片机,需要在Preferences添加开发板支持:
https://raw.githubusercontent.com/DeqingSun/ch55xduino/ch55xduino/package_ch55xduino_mcs51_index.json
开发板作者通常会随项目提供多个Example项目,允许用户快速上手。其中,Blink项目会将

CH554G Blink 示例代码
/*
Blink
Turns an LED on for one second, then off for one second, repeatedly.
Most Arduinos have an on-board LED you can control. On the simpleCH552
it is attached to digital pin P3.3
If you want to know what pin the on-board LED is connected to on your Arduino
model, check the Technical Specs of your board at:
https://www.arduino.cc/en/Main/Products
modified 8 May 2014
by Scott Fitzgerald
modified 2 Sep 2016
by Arturo Guadalupi
modified 8 Sep 2016
by Colby Newman
modified 13 Jun 2020
by Deqing Sun for use with CH55xduino
This example code is in the public domain.
http://www.arduino.cc/en/Tutorial/Blink
*/
#define LED_BUILTIN 33
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
默认
LED_BUILTIN的值为 33,即P3.3管脚。你可以随意修改其它值。
操作步骤:准备好烧录命令,断电时按住BOOT键通电进入下载模式,松手,立即点Upload按钮。
CH554G的下载模式可维持数秒,不会在Arduino IDE等扫描间隔慢的软件中看到串口设备。需要在窗口时期执行烧录命令。
烧录完成后,需要断开重连,进入普通工作模式。

选择
菜单--Sketch--Export Compiled Binary,导出.hex文件到构建目录选择
菜单--Sketch--Show Sketch Folder,在build/CH55xDuino.mcs51.ch552目录中找到 Blink.ino.hex文件
3.5. Intel HEX 转 Binary格式
SDCC只能输出Intel HEX格式的产物,可以使用Linux中的objcopy进行转换。
自行替换
led.ihx、led.bin为实际文件名。Linux
objcopy --input-target=ihex --output-target=binary led.ihx led.binmacOS
srec_cat led.ihx -intel -o led.bin -binary将
Blink.ino.bin拖拽到IDA Pro中,选择处理器类型为8051、内存架构为默认、设备名称也为默认C517。 没有提到的设置均为默认,这是个很小的项目,不需要复杂的设置。
完成后你将进入IDA Pro主界面,稍等几秒后,Output窗口提示首次分析完成,再开始操作。

The initial autoanalysis has been finished.在空白处右键,选择Text View,切换到文本视图。

从此处开始,我将逐行分析由Arduino和ch55xduino项目共同生成的这个Blink项目二进制产物。

值得留意的是,相较于从零编写的项目,平台代码为了兼容性和项目完整性会在构建产物中引入一些未启用的功能。 当你看到一些代码实际上“什么都没有做”时,不必惊讶和疑惑,这是正常现象。
为了加速阅读,你可以和我一样,在此过程中充分利用AI帮你进行代码分析,但仍推荐确保自己真的对每一处细节都有充分掌握,避免囫囵吞枣。
code:00000000 ; RESET
code:00000000
code:00000000 ; public RESET
code:00000000 RESET:
code:00000000
code:00000000 ; FUNCTION CHUNK AT code:00000066 SIZE 00000003 BYTES
code:00000000 ; FUNCTION CHUNK AT code:0000006B SIZE 00000016 BYTES
code:00000000 ; FUNCTION CHUNK AT code:000002E1 SIZE 0000000B BYTES
code:00000000
code:00000000 ljmp IEX6 ; External interrupt 6
code:00000000 ; End of function RESET经典的开局就long jump跳转,这是底层芯片的常用习惯。
IDA Pro自动为我们填充了大量推断信息帮助我们进行逆向分析。实际上
RESET 这个标签并不存在。 你可以看到地址0x00000000并没有往前走,只有 ljmp IEX6这一行是真实存在的。双击
IEX6 IDA Pro会带你跳转到这个标签的实际位置。code:0000006B IEX6: ; CODE XREF: RESET↑j
code:0000006B mov SP, #RAM_21 ; External interrupt 6
code:0000006E lcall code_7E4
code:00000071 mov A, DPL ; Data Pointer, Low Byte
code:00000073 jz code_78
code:00000075 ljmp code_66mov SP, #RAM_21将0x21赋值给SP(Stack Pointer,栈指针)。查询芯片手册可见CH554G和通用8051的SP复位值都是0x07。
8051的地址空间中0x00 - 0x07是寄存器组0的所在,为了避免破坏寄存器组的正常运转,SP在通电时默认被重置为0x07。在Blink项目中,修改为0x21的实际作用尚不清楚,但常见一些项目设置为更大的值如0x60等。
lcall code_7E4 完成SP初始化后,long call调用函数 code_7E4。code:000007E4 code_7E4: ; CODE XREF: RESET+6E↑p
code:000007E4 mov DPL, #0 ; Data Pointer, Low Byte
code:000007E7 ret
code:000007E7 ; End of function code_7E4mov DPL, #0 将数据指针的低字节 DPL 设置为 0。DPH和DPL共同组成DPTR,DPTR用于访问xRAM。在 code_7E4 上右键,选择
Jump to xref to operand
跳转到此处的来源可能会有多个,但我们这个项目很小,只有1个。这样我们就可以在不同代码层级之间来回跳转。

code:0000006B IEX6: ; CODE XREF: RESET↑j
code:0000006B mov SP, #RAM_21 ; External interrupt 6
code:0000006E lcall code_7E4
code:00000071 mov A, DPL ; Data Pointer, Low Byte
code:00000073 jz code_78
code:00000075 ljmp code_66回到标签IEX6后,我们继续看后续代码。
mov A, DPL将前面我们设为0的DPL,赋值给了累加器A。jz code_78根据累加器A是否为零,决定是否跳转到code_78。因为A必然为零,所以这个跳转必然执行,下一行的ljmp code_66也不会有机会再执行到。此处可见Arduino平台代码对最终产物的影响,存在一些目前看来没有实际价值的执行逻辑。
code:00000078 code_78: ; CODE XREF: RESET+73↑j
code:00000078 mov R1, #0x10
code:0000007A mov A, R1
code:0000007B orl A, #0
code:0000007D jz code_9A
code:0000007F mov R2, #1
code:0000007F ; END OF FUNCTION CHUNK FOR RESET
code:0000007F ; ---------------------------------------------------------------------------
code:00000081 .byte 0x90
code:00000082 .byte 9mov R1, #0x10 给寄存器R1赋值0x10。CH554G和标准8051单片机一样,有4个寄存器组。根据RS1和RS0的不同,切换不同的寄存器组。 此处刚通电不久,R1指的是寄存器组0偏移为1的寄存器,即实际地址为 0x08 + 0x01 = 0x09mov A, R1 又将给寄存器R1的值0x10复制到累加器A。orl A, #0 将累加器 A 的值与立即数 0 进行按位或操作。这条指令是冗余的,因为任何数与0进行或运算,结果都是其本身。执行后,A 的值仍然是 0x10。jz code_9A 这里累加器A的值为0x10,不会跳转。mov R2, #1 给寄存器R2赋值0x01。.byte 0x90 这里出现了未能正确识别的指令,IDA Pro只能以十六进制 0x90的方式直接展示给我们。出现这样的问题,说明我们选择的处理器型号不够准确。关闭IDA Pro,再重新打开。这一次,IDA Pro会询问是否要继续使用上次的缓存,我们选择Overwrite,重新选择设备型号。

我们仍然选择Intel 8051,但在最后选择device name时,不再选择C517,而是选择
C515。
我们重新定位到code:00000081,可以看到汇编指令已经正确解读。
code:00000078 code_78: ; CODE XREF: IEX6+8↑j
code:00000078 mov R1, #0x10
code:0000007A mov A, R1
code:0000007B orl A, #0
code:0000007D jz code_9A
code:0000007F mov R2, #1
code:00000081 mov DPTR, #0x975
code:00000084 mov R0, #0xD9
code:00000086 mov P2, #0 ; Port 2mov DPTR, #0x975 前面我们单独把DPL设置为0,这次我们将整个DPTR设置为0x975。mov R0, #0xD9 设置寄存器R0为0xD9。上面这两个操作和数值,目前没有看到实际意义。
mov P2, #0 CH554G的外部数据存储空间共64KB,其中1KB用于片内扩充xRAM,剩余保留(也就是没做)。 8051单片机访问外部数据存储空间时,自动使用P2寄存器作为高8位,因此在CH554G开发板上只能看到P1和P3的管脚,没有P2。此处清空P2的意义未知,Blink项目也用不到xRAM。留意code_78来的时候用的是jump而不是call,当然就不会有ret,所以它会继续向下执行。
code:00000089 code_89: ; CODE XREF: IEX6:code_93↓j
code:00000089 ; IEX6+2A↓j
code:00000089 clr A
code:0000008A movc A, @A+DPTR
code:0000008B movx @R0, A
code:0000008C inc DPTR
code:0000008D inc R0
code:0000008E cjne R0, #0, code_93
code:00000091 inc P2 ; Port 2clr A清空累加器Amovc A, @A+DPTR普通的mov指令用于在寄存器之间搬运数据,movc指令用于在寄存器和程序存储器之间搬运数据。 程序存储器,正是我们此刻在阅读的code:0000xxxx的区域。二进制固件会被烧录到ROM中,ROM就是程序存储器。CH554G上ROM有16KB,采用iFlash制造工艺,只可擦写约200次。我们在学习CH554G过程中要留意这件事,iFlash损坏后就需要购买新的开发板。 此刻DPTR为0x975,累加器A被清空,所以是将0x975的内容搬运到累加器A。跳转到code:00000975,可知累加器A的值为0xE1。
code:00000972 .byte 0xFF
code:00000973 .byte 0xFF
code:00000974 .byte 0xFF
code:00000975 .byte 0xE1
code:00000976 .byte 2
code:00000977 .byte 0
code:00000978 .byte 0movx @R0, Amovx指令将累加器A的值写入到xRAM中。结合上面的内容,xRAM的地址高8位由P2提供,P2已清空。R0当前值为0xD9。即,搬运0xE1到累加器A,再搬运到xRAM地址0xD9inc DPTRDPTR向下挪了1个字节。inc R0R0也向下挪了一个字节。cjne R0, #0, code_93 如果R0和0x00不相等,就跳转到code_93。code:00000093 code_93: ; CODE XREF: IEX6+23↑j
code:00000093 djnz R1, code_89
code:00000095 djnz R2, code_89
code:00000097 mov P2, #0xFF ; Port 2djnz R1, code_89 (Decrement and Jump if Not Zero) 将寄存器 R1 的值减1。如果结果不为0,则跳转回 code_89。djnz R2, code_89 如果上一条 djnz 没有跳转(即 R1 减到了0),则执行此指令。将 R2 的值减1。如果结果不为0,则跳转回 code_89。R2 = 0x01,所以执行到此处时R2 - 1一定为0,一定不会跳转 code_89。mov P2, #0xFF 当 R1 和 R2 都减到0后,循环结束,此时DPTR = 0x985,程序执行到这里。将P2端口的所有引脚设置为1。功能:将程序存储器(ROM)中从 0x975 开始的 16字节 数据,拷贝到外部数据存储器(xRAM)中从 0xD9 开始的区域。
code:0000009A code_9A: ; CODE XREF: IEX6+12↑j
code:0000009A clr A
code:0000009B mov R0, #0xFFclr A 清空累加器A。mov R0, #0xFF 填满寄存器R0。code:0000009D code_9D: ; CODE XREF: IEX6+33↓j
code:0000009D mov @R0, A
code:0000009E djnz R0, code_9D
code:000000A0 mov R0, #0
code:000000A2 mov A, R0
code:000000A3 orl A, #0
code:000000A5 jz code_B1
code:000000A7 mov R1, #0x94
code:000000A9 mov P2, #0 ; Port 2
code:000000AC clr Amov @R0, A 向iRAM地址0xFF的位置写入0x00。djnz R0, code_9D R0 = R0 - 1。重复循环,直到把iRAM完全置零。在CH554G上,iRAM仅256B,这个过程不会持续很久。mov R0, #0 循环结束后,将 R0 重新设置为 0。mov A, R0 让累加器A也变成0。orl A, #0 冗余操作:如前所述,这条指令不改变 A 的值,A 仍为 0。jz code_B1 累加器A是0x00,一定会跳转到code_B1。code:000000B1 code_B1: ; CODE XREF: IEX6+3A↑j
code:000000B1 mov R0, #0x45 ; 'E'
code:000000B3 mov A, R0
code:000000B4 orl A, #0
code:000000B6 jz code_C4
code:000000B8 mov R1, #1
code:000000BA mov DPTR, #0x94
code:000000BD clr Amov R0, #0x45 给R0赋值0x45mov A, R0 给累加器A也赋值0x45orl A, #0 冗余指令。累加器A不变。jz code_C4 A是0x45,一定不会跳转。mov R1, #1 给R1赋值为0x01mov DPTR, #0x94 给DPTR赋值0x94clr A 清空Acode:000000BE code_BE: ; CODE XREF: IEX6+55↓j
code:000000BE ; IEX6+57↓j
code:000000BE movx @DPTR, A
code:000000BF inc DPTR
code:000000C0 djnz R0, code_BE
code:000000C2 djnz R1, code_BE从此处开始,我已经习惯了一部分常见汇编操作。因此,后续简单的代码块,就转为整块解读,但阅读时仍会逐行认真分析。
code_B1 和 code_BE 共同完成了一个任务:将外部RAM从地址 0x0094 开始的连续 69 个字节全部填充为 0。
code:000000C4 code_C4: ; CODE XREF: IEX6+4B↑j
code:000000C4 mov R0, #8
code:000000C6 clr A
code:000000C7 mov @R0, A
code:000000C8 inc R0
code:000000C9 mov @R0, A
code:000000CA inc R0
code:000000CB mov @R0, A
code:000000CC inc R0
code:000000CD mov @R0, A
code:000000CE mov R0, #0xC
code:000000D0 mov @R0, #0
code:000000D2 clr RAM_20.0
code:000000D4 clr RAM_20.1
code:000000D6 ljmp code_66从0x08开始,将iRAM地址范围0x08-0x0B置零。再将0x0C也置零。
清零iRAM地址 0x20 的第0位和第1位。
跳转到code_66。
code:00000066 code_66: ; CODE XREF: IEX6+A↓j
code:00000066 ; IEX6+6B↓j
code:00000066 ljmp code_2E1长跳转到code_2E1
code:000002E1 code_2E1: ; CODE XREF: IEX6:code_66↑j
code:000002E1 lcall code_615
code:000002E4 lcall code_DA长调用 code_615 和 code_DA,我们先看 code_615。
code:00000615 code_615: ; CODE XREF: IEX6:code_2E1↑p
code:00000615 mov RESERVED00A1, #0x55 ; 'U' ; RESERVED
code:00000618 mov RESERVED00A1, #0xAA ; RESERVED
code:0000061B mov A, #0xF8
code:0000061D anl A, IP1 ; Interrupt Priority Register 1
code:0000061F orl A, #6
code:00000621 mov IP1, A ; Interrupt Priority Register 1
code:00000623 mov RESERVED00A1, #0 ; RESERVED
code:00000626 mov DPTR, #0x1388
code:00000629 lcall code_5DA
code:0000062C mov RESERVED009E, #0x5D ; ']' ; RESERVED
code:0000062F mov RESERVED009D, #0 ; RESERVED
code:00000632 mov A, #0xF0
code:00000634 anl A, TMOD ; Timer Mode Register
code:00000636 orl A, #2
code:00000638 mov TMOD, A ; Timer Mode Register
code:0000063A anl RESERVED00C9, #0xEF ; RESERVED
code:0000063D mov TH0, #6 ; Timer 0, High Byte
code:00000640 clr TF0 ; Timer 0/1 Control Register
code:00000642 setb ET0 ; Interrupt Enable Register 0
code:00000644 setb TR0 ; Timer 0/1 Control Register
code:00000646 setb EAL ; Interrupt Enable Register 0
code:00000648 ret
code:00000648 ; End of function code_615RESERVED00A1 留意末尾地址0xA1,查询特殊功能寄存器表,可知它是SAFE_MOD。
向安全模式控制寄存器投喂0x55AA(微软还把它用于MBR零扇区末端),就可以进入一段时间安全模式,提前写入任意值可以退出。

读取中断优先级控制寄存器IP1,清空低3位,再设置低3位为110b,写回IP1。
查询芯片手册,可以看到IP1每一位的作用。

在这个Blink项目中,让LED闪烁用到了delay函数,一定会用到定时器。这里可以留意定时器0的中断被打开了。
给安全模式控制寄存器投喂0x00退出安全模式后,设置DPTR为0x1388,然后长调用code_5DA。调用完成后我们还会回来的。
code:000005DA code_5DA: ; CODE XREF: code_615+14↓p
code:000005DA mov R6, DPL ; Data Pointer, Low Byte
code:000005DC mov R7, DPH ; Data Pointer, High Byte
code:000005DE clr C
code:000005DF mov A, #1
code:000005E1 subb A, R6
code:000005E2 clr A
code:000005E3 subb A, R7
code:000005E4 jc code_5E8
code:000005E6 ret复制DPTR到寄存器R6和R7,R6为0x88,R7为0x13。清空进位寄存器C。
计算 A = A - R6 - C。即 1 - 0x88 - 0。结果为负,产生借位,C 被置为 1。
计算 A = A - R7 - C。即 0 - 0x13 - 1。结果为负,C 保持为 1。
Jump if Carry,此时一定Carry,跳转到code_5E8
code:000005E8 code_5E8: ; CODE XREF: code_5DA+A↑j
code:000005E8 clr C
code:000005E9 mov A, #2
code:000005EB subb A, R6
code:000005EC mov R6, A
code:000005ED mov A, #0
code:000005EF subb A, R7
code:000005F0 mov R7, A
code:000005F1 nop
code:000005F2 cjne R6, #0, code_5FA
code:000005F5 cjne R7, #0, code_5FA
code:000005F8 nop
code:000005F9 retR6 = A = 2 - R6 = 0x02 - 0x88
R7 = A = 0 - R7 = 0x00 - 0x13
当前无法判断这样计算的意义,但可以确定的是R6和R7都不为零,跳转到code_5FA
code:000005FA code_5FA: ; CODE XREF: code_5DA+18↑j
code:000005FA ; code_5DA+1B↑j ...
code:000005FA nop
code:000005FB nop
code:000005FC nop
code:000005FD nop
code:000005FE nop
code:000005FF nop
code:00000600
code:00000600 code_600: ; CODE XREF: code_5DA+36↓j
code:00000600 nop
code:00000601 nop
code:00000602 nop
code:00000603 nop
code:00000604 nop
code:00000605 nop
code:00000606 nop
code:00000607 nop
code:00000608 nop
code:00000609 nop
code:0000060A nop
code:0000060B inc R6
code:0000060C cjne R6, #0, code_5FA
code:0000060F inc R7
code:00000610 cjne R7, #0, code_600
code:00000613 nop
code:00000614 ret大量的nop空指令,然后R6+1,再判断是否为零,不为零就跳回 code_5FA。这里形成了一个耗时的操作,但按照Blink项目源码,还没有到delay函数执行的时间,应当并不是在做模拟延时。而且CH554G有Timer,正常应当使用Timer。
只有当R6和R7都变成0,这段代码才会执行结束。
最后执行ret时需要留意:code_5FA 是jump过来的,不是call过来的,所以这里的ret等同于 code_5E8 末尾的ret。而 code_5E8 也是jump过来的,所以等同于 code_5DA 末尾的ret。code_615 中lcall code_5DA,所以会从地址0000062C继续执行。
code:00000615 code_615: ; CODE XREF: IEX6:code_2E1↑p
code:00000615 mov RESERVED00A1, #0x55 ; 'U' ; RESERVED
code:00000618 mov RESERVED00A1, #0xAA ; RESERVED
code:0000061B mov A, #0xF8
code:0000061D anl A, IP1 ; Interrupt Priority Register 1
code:0000061F orl A, #6
code:00000621 mov IP1, A ; Interrupt Priority Register 1
code:00000623 mov RESERVED00A1, #0 ; RESERVED
code:00000626 mov DPTR, #0x1388
code:00000629 lcall code_5DA
code:0000062C mov RESERVED009E, #0x5D ; ']' ; RESERVED
code:0000062F mov RESERVED009D, #0 ; RESERVED
code:00000632 mov A, #0xF0
code:00000634 anl A, TMOD ; Timer Mode Register
code:00000636 orl A, #2
code:00000638 mov TMOD, A ; Timer Mode Register
code:0000063A anl RESERVED00C9, #0xEF ; RESERVED
code:0000063D mov TH0, #6 ; Timer 0, High Byte
code:00000640 clr TF0 ; Timer 0/1 Control Register
code:00000642 setb ET0 ; Interrupt Enable Register 0
code:00000644 setb TR0 ; Timer 0/1 Control Register
code:00000646 setb EAL ; Interrupt Enable Register 0
code:00000648 ret
code:00000648 ; End of function code_615
0x9E是PWM 时钟分频设置寄存器的地址。分频 = 默认系统主频24MHz / 0x5D = 258KHz
0x9D是PWM控制寄存器,需要软件清零bPWM_CLR_ALL。

TMOD是Timer0/1方式寄存器,保留高4位不变,清空第4位。

然后设置低4位为0x2,即设置bT0_M1表示的定时器0模式选择高位。

0xC9是定时器2方式寄存器


anl RESERVED00C9, #0xEF清空最高位,选择使用分频时钟。mov TH0, #6 设置定时器0的高字节 TH0 为 6。低字节 TL0 在复位后默认为 0。 定时器初始值为 0x0600 (十进制1536)。此处留意TMOD设置的模式,模式2:8 位重载定时/计数器n,计数单元使用TLn,THn 作为重载计数单元。 计数从8位全为1变成全为0 时,设置溢出标志TFn,并自动从THn 加载初值。clr TF0 清空Timer0的溢出中断标志位。setb ET0 定时器0中断使能位,启用Timer0的中断。setb TR0 Timer0启动/停止位,置1 启动。留意TR0是寄存器TCON的bit 4,此时TCON = 0x10此时,Timer0正式启动,TH = 0x06,而TL = 0x00。下一周期,TL会变成0x01。TH与TL共同组成了当前的计数。
setb EAL EAL寄存器即EA寄存器,全局中断使能控制位,置1 启用。这段代码启动了Timer0。code_615 ret后,回到 code_2E1 继续执行。
code:000002E1 code_2E1: ; CODE XREF: IEX6:code_66↑j
code:000002E1 lcall code_615
code:000002E4 lcall code_DA长调用 code_DA
code:000000DA code_DA: ; CODE XREF: IEX6+279↓p
code:000000DA
code:000000DA ; FUNCTION CHUNK AT code:000002F0 SIZE 0000007D BYTES
code:000000DA
code:000000DA mov DPTR, #0x94
code:000000DD mov A, #1
code:000000DF movx @DPTR, A
code:000000E0 mov DPL, #0x21 ; '!' ; Data Pointer, Low Byte
code:000000E3 ljmp code_2F0
code:000000E3 ; End of function code_DA此处新旧代码有变化
向外部存储器xRAM地址0x94写入0x01。设置DPTR为0x21,然后长调用 code_2F0
code:000002F0 code_2F0: ; CODE XREF: code_DA+9↑j
code:000002F0 mov A, DPL ; Data Pointer, Low Byte
code:000002F2 mov R7, A
code:000002F3 mov DPTR, #0x925
code:000002F6 movc A, @A+DPTR
code:000002F7 mov R6, A
code:000002F8 mov A, R7
code:000002F9 mov DPTR, #0x8FD
code:000002FC movc A, @A+DPTR
code:000002FD mov R7, A
code:000002FE jnz code_301
code:00000300 ret累加器A和寄存器R7修改为0x21,然后设置DPTR为0x0925。从ROM地址
0x21 + 0x0925 = 0x0946处加载内容到累加器A。code:00000941 .byte 0
code:00000942 .byte 0
code:00000943 .byte 1
code:00000944 .byte 2
code:00000945 .byte 4
code:00000946 .byte 8
code:00000947 .byte 0x10
code:00000948 .byte 0x20
code:00000949 .byte 0x40 ; @
code:0000094A .byte 0x800x0946的值为0x8,累加器A为8。这里的操作似乎从ROM中查找配置文件。保存寄存器R6为0x8,然后再来一次。
恢复累加器A = R7 = 0x21。然后从
0x21 + 0x08FD = 0x091E处加载内容到累加器A并保存到寄存器R7。code:0000091B .byte 4
code:0000091C .byte 4
code:0000091D .byte 4
code:0000091E .byte 4
code:0000091F .byte 4
code:00000920 .byte 4
code:00000921 .byte 4
code:00000922 .byte 4总结:从ROM读取配置文件,设置R6为0x08,R7为0x04。不为零,跳转到 code_301。
code:00000301 code_301: ; CODE XREF: code_DA+224↑j
code:00000301 mov DPTR, #0x94
code:00000304 movx A, @DPTR
code:00000305 mov R5, A
code:00000306 jnz code_323
code:00000308 cjne R7, #2, code_314
code:0000030B mov A, R6
code:0000030C cpl A
code:0000030D mov R4, A
code:0000030E anl RESERVED0092, A ; RESERVED
code:00000310 mov A, R4
code:00000311 anl RESERVED0093, A ; RESERVED
code:00000313 ret在code_DA中我们刚给xRAM地址0x94赋值0x01,现在要读取回来,然后复制给寄存器R5。因为累加器A不为零,所以跳转到code_323。
code:00000323 code_323: ; CODE XREF: code_DA+22C↑j
code:00000323 cjne R5, #2, code_33A
code:00000326 cjne R7, #2, code_330
code:00000329 mov A, R6
code:0000032A orl RESERVED0092, A ; RESERVED
code:0000032C mov A, R6
code:0000032D orl RESERVED0093, A ; RESERVED
code:0000032F retR5不等于2,跳转到 code_33A。
code:0000033A code_33A: ; CODE XREF: code_DA:code_323↑j
code:0000033A cjne R5, #1, code_353
code:0000033D cjne R7, #2, code_348
code:00000340 mov A, R6
code:00000341 cpl A
code:00000342 anl RESERVED0092, A ; RESERVED
code:00000344 mov A, R6
code:00000345 orl RESERVED0093, A ; RESERVED
code:00000347 retR5等于1,不跳转 code_353。R7是0x04,不等于2,跳转到 code_348。
code:00000348 code_348: ; CODE XREF: code_DA+263↑j
code:00000348 cjne R7, #4, code_36C
code:0000034B mov A, R6
code:0000034C cpl A
code:0000034D anl RESERVED0096, A ; RESERVED
code:0000034F mov A, R6
code:00000350 orl RESERVED0097, A ; RESERVED
code:00000352 retR7等于4,不跳转 code_36C。A = R6 = 0x08,
cpl 指令按位取反,A = 1111_0111b。anl RESERVED0096, A清空P3端口输出模式寄存器(P3_MOD_OC) 的bit 3,作用是将P33管脚设置为推挽输出模式。

再复制一遍R6给累加器A,设置P3端口方向控制和上拉使能寄存器(P3_DIR_PU )的bit 3,作用是在推挽输出模式控制方向为输出。

ret回到 code_DA,继续执行
code:000000DA code_DA: ; CODE XREF: IEX6+279↓p
code:000000DA
code:000000DA ; FUNCTION CHUNK AT code:000002F0 SIZE 0000007D BYTES
code:000000DA
code:000000DA mov DPTR, #0x94
code:000000DD mov A, #1
code:000000DF movx @DPTR, A
code:000000E0 mov DPL, #0x21 ; '!' ; Data Pointer, Low Byte
code:000000E3 ljmp code_2F0
code:000000E3 ; End of function code_DAcode_DA已耗尽,从 code_E6执行。
code:000000E6 code_E6: ; CODE XREF: IEX6:code_2E7↓p
code:000000E6 mov DPTR, #0x95
code:000000E9 mov A, #1
code:000000EB movx @DPTR, A
code:000000EC mov DPL, #0x21 ; '!' ; Data Pointer, Low Byte
code:000000EF lcall code_3F7
code:000000F2 mov DPTR, #0x3E8
code:000000F5 clr A
code:000000F6 mov B, A ; B-Register
code:000000F8 lcall code_529
code:000000FB mov DPTR, #0x95
code:000000FE clr A
code:000000FF movx @DPTR, A
code:00000100 mov DPL, #0x21 ; '!' ; Data Pointer, Low Byte
code:00000103 lcall code_3F7
code:00000106 mov DPTR, #0x3E8
code:00000109 clr A
code:0000010A mov B, A ; B-Register
code:0000010C ljmp code_529
code:0000010C ; End of function code_E6向外部存储器xRAM地址0x95写入0x01。设置DPL为0x21,然后长跳转到 code_3F7。
code:000003F7 code_3F7: ; CODE XREF: code_E6+9↑p
code:000003F7 ; code_E6+1D↑p
code:000003F7 mov A, DPL ; Data Pointer, Low Byte
code:000003F9 mov R7, A
code:000003FA mov DPTR, #0x8D5
code:000003FD movc A, @A+DPTR
code:000003FE mov R6, A
code:000003FF mov A, R7
code:00000400 mov DPTR, #0x925
code:00000403 movc A, @A+DPTR
code:00000404 mov R5, A
code:00000405 mov A, R7
code:00000406 mov DPTR, #0x8FD
code:00000409 movc A, @A+DPTR
code:0000040A mov R7, A
code:0000040B mov A, R6
code:0000040C jz code_41B
code:0000040E mov DPL, R6 ; Data Pointer, Low Byte
code:00000410 push RAM_7
code:00000412 push RAM_5
code:00000414 lcall code_36D
code:00000417 pop RAM_5
code:00000419 pop RAM_7R7 = A = 0x21,设置DPTR = 0x08D5。加载ROM存储器地址(0x08D5 + 0x21 = 0x08F6)的内容到累加器A = 0x00。
code:000008F5 .byte 0
code:000008F6 .byte 0
code:000008F7 .byte 2
code:000008F8 .byte 0
code:000008F9 .byte 0
code:000008FA .byte 0
code:000008FB .byte 0
code:000008FC .byte 0R6 = A = 0x00,恢复A = R7 = 0x21。加载ROM存储器地址(0x0925 + 0x21 = 0x0946)的内容到累加器A = 0x08。
code:00000941 .byte 0
code:00000942 .byte 0
code:00000943 .byte 1
code:00000944 .byte 2
code:00000945 .byte 4
code:00000946 .byte 8
code:00000947 .byte 0x10
code:00000948 .byte 0x20
code:00000949 .byte 0x40 ; @
code:0000094A .byte 0x80R5 = A = 0x08,A = R7 = 0x21,加载ROM存储器地址(0x08FD + 0x21 = 0x091E)的内容到累加器A = 0x04。
code:0000091B .byte 4
code:0000091C .byte 4
code:0000091D .byte 4
code:0000091E .byte 4
code:0000091F .byte 4
code:00000920 .byte 4
code:00000921 .byte 4
code:00000922 .byte 4R7 = A = 0x21,A = R6 = 0x00。A为零,跳转 code_41B。
code:0000041B code_41B: ; CODE XREF: code_3F7+15↑j
code:0000041B mov C, EAL ; Interrupt Enable Register 0
code:0000041D clr A
code:0000041E rlc A
code:0000041F mov R6, A
code:00000420 clr EAL ; Interrupt Enable Register 0
code:00000422 cjne R7, #2, code_427
code:00000425 sjmp code_431EAL寄存器即EA寄存器,全局中断使能控制位,先前已置1启用。C = 0x01
清空A,然后rlc(Rotate Left through Carry)带上进位寄存器C一起循环左移动,A = 0x01
R6 = A = 0x01,清空EA,关闭了全局中断。
R7= 0x04,不等于0x02,跳转到 code_427。
code:00000427 code_427: ; CODE XREF: code_3F7+2B↑j
code:00000427 cjne R7, #3, code_42C
code:0000042A sjmp code_443R7不等于3,跳转到 code_42C。
ode:0000042C code_42C: ; CODE XREF: code_3F7:code_427↑j
code:0000042C cjne R7, #4, code_465
code:0000042F sjmp code_455R7等于4,跳转到 code_455。
这是一个典型的Switch语句。
code:00000455 code_455: ; CODE XREF: code_3F7+38↑j
code:00000455 mov DPTR, #0x95
code:00000458 movx A, @DPTR
code:00000459 jnz code_462
code:0000045B mov A, R5
code:0000045C cpl A
code:0000045D mov R7, A
code:0000045E anl P3, A ; Port 3
code:00000460 sjmp code_465A = 外部存储器地址0x95的值 = 0x21,累加器A不为零,跳转到 code_462
code:00000462 code_462: ; CODE XREF: code_3F7+62↑j
code:00000462 mov A, R5
code:00000463 orl P3, A ; Port 3A = R5 = 0x08,设置P3.3管脚为1。
此处对应C语言源码:
digitalWrite(LED_BUILTIN, HIGH);code:00000465 code_465: ; CODE XREF: code_3F7:code_42C↑j
code:00000465 ; code_3F7+45↑j ...
code:00000465 mov A, R6
code:00000466 jz code_46A
code:00000468 setb EAL ; Interrupt Enable Register 0
code:0000046A
code:0000046A code_46A: ; CODE XREF: code_3F7+6F↑j
code:0000046A retA = R6 = 0x01,不为零,不跳转 code_46A。开启全局中断。返回到 code_E6 继续执行
code:000000E6 code_E6: ; CODE XREF: IEX6:code_2E7↓p
code:000000E6 mov DPTR, #0x95
code:000000E9 mov A, #1
code:000000EB movx @DPTR, A
code:000000EC mov DPL, #0x21 ; '!' ; Data Pointer, Low Byte
code:000000EF lcall code_3F7
code:000000F2 mov DPTR, #0x3E8
code:000000F5 clr A
code:000000F6 mov B, A ; B-Register
code:000000F8 lcall code_529
code:000000FB mov DPTR, #0x95
code:000000FE clr A
code:000000FF movx @DPTR, A
code:00000100 mov DPL, #0x21 ; '!' ; Data Pointer, Low Byte
code:00000103 lcall code_3F7
code:00000106 mov DPTR, #0x3E8
code:00000109 clr A
code:0000010A mov B, A ; B-Register
code:0000010C ljmp code_529
code:0000010C ; End of function code_E6DPTR = 0x03E8,对于C语言源码
delay(1000);的延迟参数1000 ,清空累加器A,清空寄存器B,长调用 code_529code:00000529 code_529: ; CODE XREF: code_E6+12↑p
code:00000529 ; code_E6+26↑j
code:00000529 mov RAM_10, DPL ; Data Pointer, Low Byte
code:0000052C mov RAM_11, DPH ; Data Pointer, High Byte
code:0000052F mov RAM_12, B ; B-Register
code:00000532 mov RAM_13, A
code:00000534 lcall code_485
code:00000537 mov RAM_14, DPL ; Data Pointer, Low Byte
code:0000053A mov RAM_15, DPH ; Data Pointer, High Byte
code:0000053D mov RAM_16, B ; B-Register
code:00000540 mov RAM_17, A
在调用 code_485 前,记录DPL、DPH、B、A到工作寄存器组2组的前4个。
调用前:DPTR = 0x03E8,A = B = 0x0
code:00000485 code_485: ; CODE XREF: code_529+B↓p
code:00000485 ; code_529+46↓p
code:00000485 mov C, EAL ; Interrupt Enable Register 0
code:00000487 clr EAL ; Interrupt Enable Register 0
code:00000489 mov R0, RAM_8
code:0000048B mov R1, RAM_9
code:0000048D mov R2, RAM_A
code:0000048F mov R3, RAM_B
code:00000491 mov R4, TL0 ; Timer 0, Low Byte
code:00000493 mov B, TCON ; Timer 0/1 Control Register
code:00000496 mov EAL, C ; Interrupt Enable Register 0
code:00000498 jnb B5, code_4AD ; B-Register
code:0000049B mov A, #1
code:0000049D add A, R4
code:0000049E jz code_4AD
code:000004A0 inc R0
code:000004A1 cjne R0, #0, code_4AD
code:000004A4 inc R1
code:000004A5 cjne R1, #0, code_4AD
code:000004A8 inc R2
code:000004A9 cjne R2, #0, code_4AD
code:000004AC inc R3C = EAL = 0x01,然后关闭全局中断。
在 code_C4 中,我们曾经将0x08-0x0B清空,这也是工作寄存器组1的前4个,现在复制到R0-R3,使R0-R3均为0。
TL0从前面Timer0启动后一直在变化,当前值未知,假设当前值为0x57,保存到寄存器R4。

TCON当前值为0x10,保存到寄存器B。重新启用全局中断。
检查B.5,即TCON.5,也即TF0 = 0x00,jnb(Jump if Not Bit set)跳转 code_4AD
code:000004AD code_4AD: ; CODE XREF: code_485+13↑j
code:000004AD ; code_485+19↑j ...
code:000004AD clr C
code:000004AE mov A, R4
code:000004AF subb A, #6
code:000004B1 mov R4, A
code:000004B2 mov A, R4
code:000004B3 clr C
code:000004B4 rrc A
code:000004B5 mov R4, A
code:000004B6 mov B, #0x7D ; '}' ; B-Register
code:000004B9 mov A, R0
code:000004BA mul AB
code:000004BB mov R0, A
code:000004BC mov R5, B ; B-Register
code:000004BE mov B, #0x7D ; '}' ; B-Register
code:000004C1 mov A, R1
code:000004C2 mul AB
code:000004C3 add A, R5
code:000004C4 mov R1, A
code:000004C5 clr A
code:000004C6 addc A, B ; B-Register
code:000004C8 mov R5, A
code:000004C9 mov B, #0x7D ; '}' ; B-Register
code:000004CC mov A, R2
code:000004CD mul AB
code:000004CE add A, R5
code:000004CF mov R2, A
code:000004D0 clr A
code:000004D1 addc A, B ; B-Register
code:000004D3 mov R5, A
code:000004D4 mov B, #0x7D ; '}' ; B-Register
code:000004D7 mov A, R3
code:000004D8 mul AB
code:000004D9 add A, R5
code:000004DA mov R3, A
code:000004DB mov R5, #0
code:000004DD mov A, R4
code:000004DE add A, R0
code:000004DF mov DPL, A ; Data Pointer, Low Byte
code:000004E1 mov A, R1
code:000004E2 addc A, R5
code:000004E3 mov DPH, A ; Data Pointer, High Byte
code:000004E5 mov A, R2
code:000004E6 addc A, R5
code:000004E7 mov B, A ; B-Register
code:000004E9 mov A, R3
code:000004EA addc A, R5
code:000004EB ret清除寄存器C,A = R4 = 0x57。
A = A - 6 = 0x51,再存回寄存器R4。清除寄存器C,
rrc A ,此时寄存器C为0,A = A >> 1 = 0x28,再保存到寄存器R4中。B = 0x7D,A = R0 = 0x00。
mul AB,乘法,结果高8位在寄存器B,低8位在累加器A。B = 0x00,A = 0x00。R0 = A = 0x00,R5 = B = 0x00,B = 0x7D,A = R1 = 0x00。
再相乘一次,B = 0x00,A = 0x00。A = A + R5 = 0x00,R1 = A = 0x00。
清除累加器A,A = A + B + C = 0x00,R5 = A = 0x00
B = 0x7D,A = R2 = 0x00。相乘,B = 0x00, A = 0x00
A = A + R5 = 0x00,R2 = A = 0x00,清除累加器A。
A = A + B + C = 0x00,R5 = A = 0x00。
B = 0x7D,A = R3 = 0x00。相乘,B = 0x00,A = 0x00。
A = A + R5 = 0x00 + 0x00 = 0x00,R3 = A = 0x00。
R5 = 0,A = R4 = 0x28。A = A + R0 = 0x28 + 0x00 = 0x28
DPL = A = 0x28。
A = R1 = 0x00,A = A + R5 = 0x00
DPH = A = 0x00
A = R2 = 0x00,A = A + R5 = 0x00,B = A = 0x00,A = R3 = 0x00,A = A + R5 = 0x00
ret回到 code_529
code:00000529 code_529: ; CODE XREF: code_E6+12↑p
code:00000529 ; code_E6+26↑j
code:00000529 mov RAM_10, DPL ; Data Pointer, Low Byte
code:0000052C mov RAM_11, DPH ; Data Pointer, High Byte
code:0000052F mov RAM_12, B ; B-Register
code:00000532 mov RAM_13, A
code:00000534 lcall code_485
code:00000537 mov RAM_14, DPL ; Data Pointer, Low Byte
code:0000053A mov RAM_15, DPH ; Data Pointer, High Byte
code:0000053D mov RAM_16, B ; B-Register
code:00000540 mov RAM_17, A在调用 code_485 后,记录DPL、DPH、B、A到工作寄存器组2组的后4个。
code:00000542 code_542: ; CODE XREF: code_529+3C↓j
code:00000542 ; code_529+76↓j
code:00000542 mov A, RAM_10
code:00000544 orl A, RAM_11
code:00000546 orl A, RAM_12
code:00000548 orl A, RAM_13
code:0000054A jnz code_54D
code:0000054C ret使用bitwise or 检查调用 code_485 前的 DPL、DPH、B、A 是否全部为零。不是,跳转到 code_54D
code:0000054D code_54D: ; CODE XREF: code_529+21↑j
code:0000054D mov R0, RAM_10
code:0000054F mov R1, RAM_11
code:00000551 mov R2, RAM_12
code:00000553 mov R3, RAM_13
code:00000555 mov RAM_18, RAM_14
code:00000558 mov RAM_19, RAM_15
code:0000055B mov RAM_1A, RAM_16
code:0000055E mov RAM_1B, RAM_17恢复DPL、DPH、B、A到R0-R3,复制调用 code_485 后的备份到工作寄存器组3组的前4个
code:00000561 code_561: ; CODE XREF: code_529+AF↓j
code:00000561 mov A, R0
code:00000562 orl A, R1
code:00000563 orl A, R2
code:00000564 orl A, R3
code:00000565 jz code_542
code:00000567 push RAM_3
code:00000569 push RAM_2
code:0000056B push RAM_1
code:0000056D push RAM_0
code:0000056F lcall code_485
code:00000572 mov R4, DPL ; Data Pointer, Low Byte
code:00000574 mov R5, DPH ; Data Pointer, High Byte
code:00000576 mov R6, B ; B-Register
code:00000578 mov R7, A
code:00000579 pop RAM_0
code:0000057B pop RAM_1
code:0000057D pop RAM_2
code:0000057F pop RAM_3
code:00000581 mov A, R4
code:00000582 clr C
code:00000583 subb A, RAM_18
code:00000585 mov R4, A
code:00000586 mov A, R5
code:00000587 subb A, RAM_19
code:00000589 mov R5, A
code:0000058A mov A, R6
code:0000058B subb A, RAM_1A
code:0000058D mov R6, A
code:0000058E mov A, R7
code:0000058F subb A, RAM_1B
code:00000591 mov R7, A
code:00000592 clr C
code:00000593 mov A, R4
code:00000594 subb A, #0xE8
code:00000596 mov A, R5
code:00000597 subb A, #3
code:00000599 mov A, R6
code:0000059A subb A, #0
code:0000059C mov A, R7
code:0000059D subb A, #0
code:0000059F jc code_542
code:000005A1 dec R0
code:000005A2 cjne R0, #0xFF, code_5AE
code:000005A5 dec R1
code:000005A6 cjne R1, #0xFF, code_5AE
code:000005A9 dec R2
code:000005AA cjne R2, #0xFF, code_5AE
code:000005AD dec R3再检查一次R0-R3是否全部为零,不是,不跳转 code_542。

push RAM_3其实是 push 0x03,BANK0_R3,也就是当前的R3。R3-R0全部压栈备份,然后长调用 code_485。
code:00000485 code_485: ; CODE XREF: code_529+B↓p
code:00000485 ; code_529+46↓p
code:00000485 mov C, EAL ; Interrupt Enable Register 0
code:00000487 clr EAL ; Interrupt Enable Register 0
code:00000489 mov R0, RAM_8
code:0000048B mov R1, RAM_9
code:0000048D mov R2, RAM_A
code:0000048F mov R3, RAM_B
code:00000491 mov R4, TL0 ; Timer 0, Low Byte
code:00000493 mov B, TCON ; Timer 0/1 Control Register
code:00000496 mov EAL, C ; Interrupt Enable Register 0
code:00000498 jnb B5, code_4AD ; B-Register
code:0000049B mov A, #1
code:0000049D add A, R4
code:0000049E jz code_4AD
code:000004A0 inc R0
code:000004A1 cjne R0, #0, code_4AD
code:000004A4 inc R1
code:000004A5 cjne R1, #0, code_4AD
code:000004A8 inc R2
code:000004A9 cjne R2, #0, code_4AD
code:000004AC inc R3我们再次回到 code_485,从此处开始不再继续跟进,因为看起来是个死循环,对应Arduino的loop()函数
code:0000000B ; Timer 0 overflow
code:0000000B ; Attributes: thunk
code:0000000B
code:0000000B ; public TF0
code:0000000B TF0:
code:0000000B ljmp TF0_0
code:0000000B ; End of function TF0当TL0从0x06增长到0xFF,期间一共250个周期(256 - 6 = 250),按照 1个周期 = 1us 计算,则每次中断溢出耗时0.25ms。1000ms 需要完成 40000次中断溢出。
这是因为当前样例代码仅仅使用TL0作为计数器,使用TH0存储自动重置初始值,极大地增加了中断次数。
发生Timer0溢出中断时,8051单片机会固定加载0x0B地址的内容作为中断处理程序。
code:0000046C TF0_0: ; CODE XREF: TF0↑j
code:0000046C push PSW ; Program Status Word Register
code:0000046E mov PSW, #8 ; Program Status Word Register
code:00000471 inc R0
code:00000472 cjne R0, #0, code_482
code:00000475 inc R1
code:00000476 cjne R1, #0, code_482
code:00000479 inc R2
code:0000047A cjne R2, #0, code_482
code:0000047D inc R3
code:0000047E cjne R3, #0, code_482
code:00000481 inc R4
code:00000482
code:00000482 code_482: ; CODE XREF: TF0_0+6↑j
code:00000482 ; TF0_0+A↑j ...
code:00000482 pop PSW ; Program Status Word Register
code:00000484 retimov PSW, #8 RS1=1, RS0=0,这会选择工作寄存器组1,对应物理地址 0x08-0x0FTF0_0 在每次中断时,将R0-R4作为一个巨大的整数,不断 +1,然后结束中断执行。它不会立刻执行P3管脚的任务,而是等待主程序在循环中再次读取物理地址 0x08-0x0F 的信息,判断是否要进行具体操作。
样例代码在主程序的循环中存在逻辑错误,造成延迟时间错误。尽管如此,本文以汇编作为切入点,逐行阅读和分析8051单片机指令的任务目标已经达成。
引用: https://www.bilibili.com/video/BV1RM4y1a7J5
一位数码管原理非常简单,它内部有7个LED灯组成数字8的所有“笔划”,再加上小数点共有8个LED灯。 所有8个LED灯的GND接到一起,然后一起接到主电路的GND。只需要控制8个LED灯的管脚电压(高电平为电量,低电平为熄灭),就能显示任意内容。按照固定组合(LUT查找表),就能显示正确的阿拉伯数字。
数码显示数字 Demo
int pins[8] = {23, 22, 1, 3, 21, 19, 18, 5};
int number_array[][8] = {
//a, b, c, d, e, f, g, dp
{0, 0, 0, 0, 0, 0, 1, 1}, // 0
{1, 0, 0, 1, 1, 1, 1, 1}, // 1
{0, 0, 1, 0, 0, 1, 0, 1}, // 2
{0, 0, 0, 0, 1, 1, 0, 1}, // 3
{1, 0, 0, 1, 1, 0, 0, 1}, // 4
{0, 1, 0, 0, 1, 0, 0, 1}, // 5
{0, 1, 0, 0, 0, 0, 0, 1}, // 6
{0, 0, 0, 1, 1, 1, 1, 1}, // 7
{0, 0, 0, 0, 0, 0, 0, 1}, // 8
{0, 0, 0, 0, 1, 0, 0, 1}, // 9
};
void setup() {
for (int i=0;i<8;i++){
pinMode(pins[i], OUTPUT);
digitalWrite(pins[i], HIGH);
}
}
void loop(){
int num = 6;
for (int i=0;i<8;i++) {
digitalWrite(pins[i], number_array[num][i]);
}
}

多位数码管原理和一位数码管类似,只不过多了个位选管脚,用来控制哪一位数码管亮起。这里要注意,4位数码管不是4个1位数码管放在一起单独控制,它实际上只有同时点亮1位数码管的能力。
1位数码管需要8个控制引脚和1个GND,如果简单并联就会需要32个控制引脚和1个GND,这在嵌入式开发引脚极为宝贵的场景下是不现实的。多位数码管是以极高的刷新率依次点亮单个数码管,通过视觉暂留现象让人看起来像是同时点亮。
这样4位数码管就只需要8个控制引脚 + 4个GND。4个GND分别决定点亮哪一个数码管。这种做法大大节省了引脚资源。但是,由于刷新率较高,所以需要控制好每个数码管的显示时间,否则会看到闪烁的现象。
4位数码显示数字 Demo
int seg_array[4] = {5, 18, 19, 21};
int led_array[8] = {32, 25, 27, 12, 13, 33, 26, 14};
int logic_array[10][8] = {
//a, b, c, d, e, f, g, dp
{1, 1, 1, 1, 1, 1, 0, 0}, // 0
{0, 1, 1, 0, 0, 0, 0, 0}, // 1
{1, 1, 0, 1, 1, 0, 1, 0}, // 2
{1, 1, 1, 1, 0, 0, 1, 0}, // 3
{0, 1, 1, 0, 0, 1, 1, 0}, // 4
{1, 0, 1, 1, 0, 1, 1, 0}, // 5
{1, 0, 1, 1, 1, 1, 1, 0}, // 6
{1, 1, 1, 0, 0, 0, 0, 0}, // 7
{1, 1, 1, 1, 1, 1, 1, 0}, // 8
{1, 1, 1, 1, 0, 1, 1, 0}, // 9
};
void clear(){
for (int i=0;i<4;i++) {
digitalWrite(seg_array[i], HIGH);
}
for (int i=0;i<8;i++){
digitalWrite(led_array[i], LOW);
}
}
void display_number(int order, int number) {
clear();
digitalWrite(seg_array[order], LOW);
for (int i=0;i<8;i++) {
digitalWrite(led_array[i], logic_array[number][i]);
}
}
void display_4_number(int number) {
if (number < 10000){
int number_array[4];
for (int i=3;i>=0;i--) {
number_array[i] = number % 10;
number /= 10;
}
for (int i=0;i<4;i++) {
display_number(i, number_array[i]);
delay(5);
}
}
}
void setup() {
for (int i=0;i<4;i++) {
pinMode(seg_array[i], OUTPUT);
digitalWrite(seg_array[i], HIGH);
}
for (int i=0;i<8;i++){
pinMode(led_array[i], OUTPUT);
digitalWrite(led_array[i], LOW);
}
}
void loop() {
display_4_number(4567);
}
消除抖动:使用硬件或者软件消除按下和松开瞬间的电平抖动问题。硬件消抖一般使用RS触发器或电容,软件消抖则通过间隔约10ms两次检测均相同的来判定。
管脚设置:需要将对应管脚设置为输入模式,并指定是上拉电阻还是下拉电阻
pinMode(button_pin, INPUT_PULLDOWN);按键检测
int led_pin = 18;
int button_pin = 22;
int led_logic = 0;
bool status = false;
void setup() {
pinMode(led_pin, OUTPUT);
pinMode(button_pin, INPUT_PULLDOWN);
}
void loop() {
if (digitalRead(button_pin)) {
delay(10);
if (digitalRead(button_pin) && !status) {
led_logic = !led_logic;
digitalWrite(led_pin, led_logic);
status = !status;
} else if (!digitalRead(button_pin)) {
status = false;
}
}
}
PWM(Pulse Width Modulation)通过调整高电平在整个时间段中的占比,来调整总的输出功率。例如80%的时间为高电平,20%时间为低电平,则占空比为20%,总输出功率是瞬间功率在时间上的积分,功率为80%。
除了占空比决定总功率,还可以控制高电平和低电平交替的频率。在总功率不变的情况下,频率越高波形的锯齿数量就越多,功率输出就越“细腻”。
PWM LED灯 Demo
#define FREQ 2000 // 频率
#define CHANNEL 0 // 通道
#define RESOLUTION 8 // 分辨率
#define LED 18 // LED 引脚
void setup() {
ledcAttach(LED, FREQ, RESOLUTION);
}
void loop() {
for (int i=0;i<pow(2, RESOLUTION);i++) {
ledcWrite(LED, i);
delay(10);
}
for (int i=pow(2,RESOLUTION)-1;i>=0;i--) {
ledcWrite(LED, i);
delay(10);
}
}

12864液晶屏显示 Demo
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// 定义OLED屏幕参数(0.96寸通常分辨率是128x64)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // 重置引脚,-1表示共享Arduino重置引脚
// 初始化SSD1306对象,使用I2C通信
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(115200);
// 初始化OLED显示屏,I2C地址通常为0x3C或0x3D
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306分配失败"));
for(;;); // 卡在这里
}
Serial.println("OLED初始化成功!");
// 清空缓冲区
display.clearDisplay();
display.setTextSize(2); // 设置文字大小
display.setTextColor(SSD1306_WHITE); // 设置文字颜色(白色)
display.setCursor(0, 0); // 设置光标位置(x, y)
display.println("Hello"); // 显示英文
display.setCursor(0, 20); // 下移光标
display.println("World!");
display.setCursor(0, 40); // 继续下移
display.setTextSize(1); // 使用小号字体显示中文
display.println("Aiden Leong");
// 将缓冲区内容发送到显示屏
display.display();
delay(2000); // 显示2秒
}
void loop() {
// 清屏
display.clearDisplay();
// 显示动态内容 - 模拟计数器
static int counter = 0;
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Counter: ");
display.println(counter++);
display.setCursor(0, 20);
display.println("Aiden Leong");
// 画一条线
display.drawLine(0, 40, 127, 40, SSD1306_WHITE);
// 画一个矩形
display.drawRect(10, 45, 50, 15, SSD1306_WHITE);
// 显示
display.display();
delay(1000); // 每秒更新一次
}

U8G2 Demo
#include <Arduino.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
void setup() {
Serial.begin(115200);
u8g2.begin();
Serial.println("OLED (U8G2) 初始化成功!");
u8g2.enableUTF8Print();
// --- setup() 中的静态显示 ---
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_profont17_tr);
u8g2.drawStr(0, 20, "Hello");
u8g2.drawStr(0, 40, "World!");
u8g2.setFont(u8g2_font_profont12_tr);
u8g2.drawStr(0, 60, "Aiden Leong");
} while ( u8g2.nextPage() );
delay(2000); // 显示2秒
}
void loop() {
// --- loop() 中的动态显示 ---
u8g2.firstPage();
do {
static int counter = 0;
// 使用小号字体显示计数器
u8g2.setFont(u8g2_font_wqy12_t_chinese2);
char counter_str[20];
snprintf(counter_str, sizeof(counter_str), "计数器: %d", counter++);
u8g2.setCursor(0, 12);
u8g2.print(counter_str);
u8g2.setCursor(0, 32);
u8g2.print("你好");
// 画线和矩形 (保持不变)
u8g2.drawLine(0, 40, 127, 40);
u8g2.drawFrame(10, 45, 50, 15);
} while ( u8g2.nextPage() );
delay(1000);
}

Linux广泛运行在大量计算设备上,从嵌入式设备到超算集群几乎无处不在。
相关内容位于
/etc/update-motd.d目录中,以shell脚本形式提供Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-79-generic aarch64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Thu Sep 18 01:34:49 AM UTC 2025
System load: 0.0
Usage of /: 11.7% of 57.25GB
Memory usage: 4%
Swap usage: 0%
Processes: 121
Users logged in: 0
IPv4 address for enp0s1: 192.168.64.2
IPv6 address for enp0s1: fd74:46d5:8a72:b02c:689d:40ff:fe98:b46c
* Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
just raised the bar for easy, resilient and secure K8s cluster deployment.
https://ubuntu.com/engage/secure-kubernetes-at-the-edge
Expanded Security Maintenance for Applications is not enabled.
19 updates can be applied immediately.
To see these additional updates run: apt list --upgradable
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
Last login: Wed Sep 3 03:22:35 2025 from 192.168.64.1特点:增量传输、断点续传、可选单向或双向同步
rsync 用法示例
rsync -avz --delete --include 'dir1/***' --include 'package.json' --exclude='*' . root@xx.xx.xx.xx:/root/remote_target/内核模块入门代码:Hello, World!
hello.c
#include <linux/init.h> // 包含模块初始化和清理函数的宏
#include <linux/module.h> // 必须包含的核心头文件,用于所有模块
#include <linux/kernel.h> // 包含内核函数,如 printk()
// 模块许可证。强烈推荐设置,否则内核会发出警告。
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module.");
MODULE_VERSION("0.1");
// 模块初始化函数
// 当使用 'insmod' 或 'modprobe' 加载模块时,这个函数会被调用
static int __init hello_init(void) {
// printk 是内核中的 printf 函数
// KERN_INFO 是日志级别,表示这是一般信息性消息
// 它会输出到内核的日志缓冲区,可以用 'dmesg' 命令查看
printk(KERN_INFO "Hello, World! The module has been loaded.\n");
return 0; // 返回 0 表示成功
}
// 模块清理函数
// 当使用 'rmmod' 卸载模块时,这个函数会被调用
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, World! The module has been unloaded.\n");
}
// 注册模块的初始化和清理函数
// module_init 告诉内核,hello_init 是加载时要执行的函数
module_init(hello_init);
// module_exit 告诉内核,hello_exit 是卸载时要执行的函数
module_exit(hello_exit);
Makefile
# obj-m 是一个变量,告诉 kbuild (内核构建系统) 我们要构建一个可加载的模块
# hello.o 是我们的目标文件名,- 表示它是一个模块,最终会生成 hello.ko
obj-m += hello.o
# all 是默认的目标
# make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
# -C: 切换到指定目录
# /lib/modules/$(shell uname -r)/build: 这是内核源代码或头文件所在的位置
# M=$(PWD): 告诉内核构建系统,我们的模块源代码在当前目录
# modules: 指定要执行的操作是编译模块
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
# clean 是清理的目标,用于删除编译过程中产生的文件
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
MODULE_LICENSE("GPL"); 用于访问EXPORT_SYMBOL_GPL导出的符号。简单来说,你必须加上这句话才可以使用内核中GPL协议的各种函数。由于Linux内核主体就是GPL协议,不加上这句话几乎意味着无法进行内核开发。这是由于Linux采用的GPL协议具有传染性,除了少数具体情况。
sudo insmod hello.komodprobe -r hello使用
lsmod命令可以列出当前系统已加载的内核模块。$ lsmod
Module Size Used by
tls 159744 0
qrtr 49152 2
cfg80211 1249280 0
snd_hda_codec_generic 114688 1
snd_hda_intel 57344 0
snd_intel_dspcfg 20480 1 snd_hda_intel
snd_hda_codec 208896 2 snd_hda_codec_generic,snd_hda_intel
binfmt_misc 28672 1
snd_hda_core 163840 3 snd_hda_codec_generic,snd_hda_intel,snd_hda_codec
snd_hwdep 24576 1 snd_hda_codec
snd_pcm 196608 3 snd_hda_intel,snd_hda_codec,snd_hda_core
snd_timer 53248 1 snd_pcm
snd 147456 6 snd_hda_codec_generic,snd_hwdep,snd_hda_intel,snd_hda_codec,snd_timer,snd_pcm
9pnet_virtio 20480 0
9pnet 106496 1 9pnet_virtio
soundcore 16384 1 snd
nls_iso8859_1 12288 1
input_leds 12288 0
joydev 36864 0
uas 32768 0
usb_storage 90112 1 uas
sch_fq_codel 24576 2
dm_multipath 49152 0
efi_pstore 12288 0
nfnetlink 20480 2
dmi_sysfs 24576 0
qemu_fw_cfg 24576 0
ip_tables 36864 0
x_tables 65536 1 ip_tables
autofs4 57344 2
btrfs 1929216 0
blake2b_generic 24576 0
hid_generic 12288 0
usbhid 81920 0
hid 184320 2 usbhid,hid_generic
raid10 77824 0
raid456 212992 0
async_raid6_recov 24576 1 raid456
async_memcpy 16384 2 raid456,async_raid6_recov
async_pq 16384 2 raid456,async_raid6_recov
async_xor 16384 3 async_pq,raid456,async_raid6_recov
async_tx 16384 5 async_pq,async_memcpy,async_xor,raid456,async_raid6_recov
xor 12288 2 async_xor,btrfs
xor_neon 16384 1 xor
raid6_pq 110592 4 async_pq,btrfs,raid456,async_raid6_recov
libcrc32c 12288 2 btrfs,raid456
raid1 61440 0
raid0 24576 0
crct10dif_ce 12288 1
polyval_ce 12288 0
polyval_generic 12288 1 polyval_ce
ghash_ce 24576 0
sm4 12288 0
sha3_ce 16384 0
sha2_ce 20480 0
sha256_arm64 24576 1 sha2_ce
sha1_ce 12288 0
virtio_gpu 94208 0
virtio_dma_buf 12288 1 virtio_gpu
virtio_rng 12288 0
xhci_pci 28672 0
xhci_pci_renesas 24576 1 xhci_pci
aes_neon_bs 24576 0
aes_neon_blk 28672 1 aes_neon_bs
aes_ce_blk 36864 0
aes_ce_cipher 12288 1 aes_ce_blklsmod的原理是读取/proc/modules文件的内容字段格式:
模块名称 内存占用 引用计数 依赖模块 模块状态 内存地址$ cat /proc/modules
tls 159744 0 - Live 0x0000000000000000
qrtr 49152 2 - Live 0x0000000000000000
cfg80211 1249280 0 - Live 0x0000000000000000
snd_hda_codec_generic 114688 1 - Live 0x0000000000000000
snd_hda_intel 57344 0 - Live 0x0000000000000000
snd_intel_dspcfg 20480 1 snd_hda_intel, Live 0x0000000000000000
snd_hda_codec 208896 2 snd_hda_codec_generic,snd_hda_intel, Live 0x0000000000000000
binfmt_misc 28672 1 - Live 0x0000000000000000
snd_hda_core 163840 3 snd_hda_codec_generic,snd_hda_intel,snd_hda_codec, Live 0x0000000000000000
snd_hwdep 24576 1 snd_hda_codec, Live 0x0000000000000000
snd_pcm 196608 3 snd_hda_intel,snd_hda_codec,snd_hda_core, Live 0x0000000000000000
snd_timer 53248 1 snd_pcm, Live 0x0000000000000000
snd 147456 6 snd_hda_codec_generic,snd_hda_intel,snd_hda_codec,snd_hwdep,snd_pcm,snd_timer, Live 0x0000000000000000
9pnet_virtio 20480 0 - Live 0x0000000000000000
9pnet 106496 1 9pnet_virtio, Live 0x0000000000000000
soundcore 16384 1 snd, Live 0x0000000000000000
nls_iso8859_1 12288 1 - Live 0x0000000000000000
input_leds 12288 0 - Live 0x0000000000000000
joydev 36864 0 - Live 0x0000000000000000
uas 32768 0 - Live 0x0000000000000000
usb_storage 90112 1 uas, Live 0x0000000000000000
sch_fq_codel 24576 2 - Live 0x0000000000000000
dm_multipath 49152 0 - Live 0x0000000000000000
efi_pstore 12288 0 - Live 0x0000000000000000
nfnetlink 20480 2 - Live 0x0000000000000000
dmi_sysfs 24576 0 - Live 0x0000000000000000
qemu_fw_cfg 24576 0 - Live 0x0000000000000000
ip_tables 36864 0 - Live 0x0000000000000000
x_tables 65536 1 ip_tables, Live 0x0000000000000000
autofs4 57344 2 - Live 0x0000000000000000
btrfs 1929216 0 - Live 0x0000000000000000
blake2b_generic 24576 0 - Live 0x0000000000000000
hid_generic 12288 0 - Live 0x0000000000000000
usbhid 81920 0 - Live 0x0000000000000000
hid 184320 2 hid_generic,usbhid, Live 0x0000000000000000
raid10 77824 0 - Live 0x0000000000000000
raid456 212992 0 - Live 0x0000000000000000
async_raid6_recov 24576 1 raid456, Live 0x0000000000000000
async_memcpy 16384 2 raid456,async_raid6_recov, Live 0x0000000000000000
async_pq 16384 2 raid456,async_raid6_recov, Live 0x0000000000000000
async_xor 16384 3 raid456,async_raid6_recov,async_pq, Live 0x0000000000000000
async_tx 16384 5 raid456,async_raid6_recov,async_memcpy,async_pq,async_xor, Live 0x0000000000000000
xor 12288 2 btrfs,async_xor, Live 0x0000000000000000
xor_neon 16384 1 xor, Live 0x0000000000000000
raid6_pq 110592 4 btrfs,raid456,async_raid6_recov,async_pq, Live 0x0000000000000000
libcrc32c 12288 2 btrfs,raid456, Live 0x0000000000000000
raid1 61440 0 - Live 0x0000000000000000
raid0 24576 0 - Live 0x0000000000000000
crct10dif_ce 12288 1 - Live 0x0000000000000000
polyval_ce 12288 0 - Live 0x0000000000000000
polyval_generic 12288 1 polyval_ce, Live 0x0000000000000000
ghash_ce 24576 0 - Live 0x0000000000000000
sm4 12288 0 - Live 0x0000000000000000
sha3_ce 16384 0 - Live 0x0000000000000000
sha2_ce 20480 0 - Live 0x0000000000000000
sha256_arm64 24576 1 sha2_ce, Live 0x0000000000000000
sha1_ce 12288 0 - Live 0x0000000000000000
virtio_gpu 94208 0 - Live 0x0000000000000000
virtio_dma_buf 12288 1 virtio_gpu, Live 0x0000000000000000
virtio_rng 12288 0 - Live 0x0000000000000000
xhci_pci 28672 0 - Live 0x0000000000000000
xhci_pci_renesas 24576 1 xhci_pci, Live 0x0000000000000000
aes_neon_bs 24576 0 - Live 0x0000000000000000
aes_neon_blk 28672 1 aes_neon_bs, Live 0x0000000000000000
aes_ce_blk 36864 0 - Live 0x0000000000000000
aes_ce_cipher 12288 1 aes_ce_blk, Live 0x0000000000000000dmesg命令用于打印内核日志,它可以从内核日志缓冲区中读取日志信息。在内核模块中,可以使用printk函数将日志信息输出到内核日志缓冲区中,然后使用dmesg命令查看这些日志信息。这里我们构建一个echo服务器,向
/proc/echo_driver写入的内容将被打印到内核日志中(使用dmesg查看)echo_proc.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h> // 用于 copy_from_user 和 copy_to_user
#include <linux/string.h>
#define MODULE_NAME "echo_proc"
#define PROC_ENTRY_NAME "echo_driver"
#define MAX_BUFFER_LEN 1024
static char proc_buffer[MAX_BUFFER_LEN];
static unsigned long proc_buffer_size = 0;
// 当用户读取 /proc/echo_driver 时,此函数被调用
static ssize_t proc_read(struct file *file, char __user *user_buf, size_t count, loff_t *offp)
{
ssize_t len = 0;
// 如果偏移量不为0,说明已经读完了,返回0表示文件结束
if (*offp > 0) {
return 0;
}
// 确保不会读取超出缓冲区大小的数据
if (count > proc_buffer_size) {
len = proc_buffer_size;
} else {
len = count;
}
// 将内核空间的数据复制到用户空间
if (copy_to_user(user_buf, proc_buffer, len) != 0) {
// 复制失败
return -EFAULT;
}
// 更新偏移量
*offp += len;
printk(KERN_INFO "echo_proc: Read %zu bytes from buffer\n", len);
return len;
}
// 当用户写入 /proc/echo_driver 时,此函数被调用
static ssize_t proc_write(struct file *file, const char __user *user_buf, size_t count, loff_t *offp)
{
ssize_t len = 0;
// 确保写入的数据不会超过缓冲区大小
if (count > MAX_BUFFER_LEN - 1) {
printk(KERN_WARNING "echo_proc: Write size %zu exceeds buffer limit\n", count);
len = MAX_BUFFER_LEN - 1;
} else {
len = count;
}
// 将用户空间的数据复制到内核空间
if (copy_from_user(proc_buffer, user_buf, len) != 0) {
// 复制失败
return -EFAULT;
}
// 在字符串末尾添加空字符,以便安全地作为字符串处理
proc_buffer[len] = '\0';
proc_buffer_size = len;
// 移除可能的换行符,方便读取时显示
if (len > 0 && proc_buffer[len - 1] == '\n') {
proc_buffer[len - 1] = '\0';
proc_buffer_size--;
}
printk(KERN_INFO "echo_proc: Wrote %zu bytes to buffer: %s\n", proc_buffer_size, proc_buffer);
return len; // 返回实际写入的字节数
}
// 定义文件操作结构体
static const struct proc_ops my_proc_ops = {
.proc_read = proc_read,
.proc_write = proc_write,
};
// 模块初始化函数
static int __init echo_proc_init(void)
{
struct proc_dir_entry *proc_entry;
// 在 /proc 下创建一个条目
proc_entry = proc_create(PROC_ENTRY_NAME, 0666, NULL, &my_proc_ops);
if (proc_entry == NULL) {
printk(KERN_ERR "echo_proc: Failed to create /proc/%s\n", PROC_ENTRY_NAME);
return -ENOMEM;
}
printk(KERN_INFO "echo_proc: Module loaded. /proc/%s created.\n", PROC_ENTRY_NAME);
return 0;
}
// 模块退出函数
static void __exit echo_proc_exit(void)
{
// 移除 /proc 条目
remove_proc_entry(PROC_ENTRY_NAME, NULL);
printk(KERN_INFO "echo_proc: Module unloaded. /proc/%s removed.\n", PROC_ENTRY_NAME);
}
module_init(echo_proc_init);
module_exit(echo_proc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple echo driver using /proc");
MODULE_VERSION("0.1");__init宏用于标记初始化函数,这些函数在模块加载时执行,并在模块卸载时释放。使用__init宏标记的函数在编译时会被放在一个特殊的段中,这个段在模块加载时会被映射到内存中,并在模块卸载时被释放。module_init宏用于标记模块初始化函数,该函数在模块加载时被调用。使用module_init宏标记的函数必须在模块加载时执行,并且必须在模块卸载时释放。printk函数用于在内核中输出日志信息。它类似于用户空间的printf函数,但是它的输出会被重定向到内核日志缓冲区,而不是标准输出。使用printk函数可以方便地调试内核模块。module_param宏用于定义模块参数,这些参数可以在模块加载时通过命令行参数进行设置。使用module_param宏定义的参数可以在模块加载时通过命令行参数进行设置,例如:insmod hello.ko param1=10 param2="hello"。与用户态C语言开发相似,在内核态你也可以打印日志,使用printk(print in kernel)
proc_create函数用于创建一个proc文件,该文件可以用于在用户空间和内核空间之间进行通信。使用proc_create函数创建的proc文件可以在/proc目录下看到,并且可以通过cat命令查看其内容。参数:
name:proc文件的名称。mode:proc文件的权限。parent: parent目录。proc_fops:proc文件的操作函数,用于定义如何读写proc文件。代码示例
struct proc_dir_entry *proc_entry;
proc_entry = proc_create(PROC_ENTRY_NAME, 0666, NULL, &my_proc_ops);
if (proc_entry == NULL) {
printk(KERN_ERR "echo_proc: Failed to create /proc/%s\n", PROC_ENTRY_NAME);
return -ENOMEM;
}使用parent参数在子目录中创建文件
struct proc_dir_entry *mydir;
mydir = proc_mkdir("mydriver", NULL); // 创建 /proc/mydriver
if (mydir) {
proc_create("status", 0444, mydir, &status_proc_ops); // 创建 /proc/mydriver/status
}proc_write函数用于接收来自proc文件的写入。当用户在用户空间中写入proc文件时,proc_write函数会被调用,并将用户写入的数据作为参数传递给该函数。printk和pr_info等pr_开头的函数都可以用来打印内核日志。二者区别仅仅在于,pr_开头的函数是printk的封装,它从字面以上就可以知道logging level,也会提供预先配置好的打印格式。在新的内核社区代码规范中,你应当优先使用pr_开头的函数,因为它们提供了更好的可读性和可维护性。
KERN_EMERG: pr_emerg()
KERN_ALERT: pr_alert()
KERN_CRIT: pr_crit()
KERN_ERR: pr_err()
KERN_WARNING: pr_warn()
KERN_NOTICE: pr_notice()
KERN_INFO: pr_info()
KERN_DEBUG: pr_debug()
KERN_DEFAULT: printk()
KERN_CONT: pr_cont()
Linux内核提供多种同步机制:自旋锁、信号量等。需要注意的是,在用户态,锁机制是由库函数提供或是开发者自己写的,C语言本身并不提供同步机制,而操作系统提供的是用于实现这些锁的基础设施。在内核态,我们使用内核源码中已经写好的各种锁,而不是重复造轮子。今后,除非特别说明,本文所说的各种锁均指内核中的锁。
如果你想看的是用户态的C语言实现,请阅读相关文章。
在现代CPU架构中,片上多级缓存(常见L1缓存独享,L2、L3缓存共享)会因为不同核心的共享数据的修改产生缓存一致性问题。在需要确保一致性的场景下,可以使用CPU提供的原子性指令。
总线锁
总线锁通过锁定CPU总线来短暂禁止其它CPU核心访问内存
Loading image...
缓存锁
缓存锁通过锁定缓存行来短暂禁止其它CPU核心访问内存
在面试时,考官喜欢拿乐观锁、悲观锁出考题。
乐观锁是一种乐观的并发控制策略,它假设并发冲突发生的概率很低,因此不会在获取锁时阻塞其他线程。相反,它会在操作数据时先尝试获取一个版本号,然后在进行数据操作时检查版本号是否发生了变化。如果版本号发生了变化,说明有其他线程已经修改了数据,此时需要重新获取数据并进行操作。
悲观锁是一种悲观的并发控制策略,它假设并发冲突发生的概率很高,因此会在获取锁时阻塞其他线程。只有获取到锁的线程才能进行数据操作,其他线程需要等待锁释放后才能进行操作。
自旋锁这个中文翻译非常不好,因为自旋在中文语义默认是个高深的物理概念,让自旋锁听起来似乎很复杂。实际上SpinLock模拟的是一个小朋友,想到得到一个玩具却被别的小朋友抢先了,他只能急得原地打转,直到占用者不玩了才可能轮到他。
SpinLock的缺点也如它的名字:未能获取资源的进程会占据着CPU原地打转(不做任何有价值的事情),直到获取到等待的资源才会继续。这一期间,CPU资源被白白浪费了,而不是交给别的进程去执行有价值的任务。
此时,它的第二个缺点也可以被合理推断出来:当A等待B,B等待C,C原本在快乐地执行任务,但突然C开始等待A。永远无法解开的锁等待状态,被称为死锁。
什么时候使用spinlock:当spinlock原地转圈的时间非常短,此时上下文切换的开销更高,此时用spinlock更合适。
RCU (Read-Copy Update) 允许同时存在1个写操作和多个读操作,适合读操作远高于写操作的场景。
19.2.1. LFS: Linux From Scratch
19.2.2. BLFS: Beyond LFS
19.3.1. Partial Mirror
19.3.2. initramfs制作
Linux中有大量的发行版,比如说Ubuntu、Debian、Arch、Manjaro、Fedora、Centos、Redhat等知名产品。 许多人并不清楚这些发行版之间的关系。本章通过介绍这些发行版,让你今后对于不同的发行版都有清晰的认识和理解。
首先,请允许我引用网上广为流传的一张发行版关系树图片。
这张图片非常巨大,但我还是决定将完整的图片放在这里。你可以在这张图中找到很多非常熟悉的发行版,以及一些古老的已经不再维护的发行版。(点击跳转大图)
Debian是Linux发行版中最古老的一个。它诞生于1993年,是Linux社区中最受欢迎的发行版之一。Debian的稳定性、安全性和可定制性使其成为许多服务器和开发环境的理想选择。
Debian的包管理器APT是其最大的特点之一。APT是一个高级包管理工具,它允许用户轻松地安装、升级和删除软件包。APT还支持自动解决软件包之间的依赖关系,这使得安装和管理软件变得更加简单。
Debian的另一个特点是它的源代码可用性。Debian的源代码是开源的,任何人都可以自由地查看、修改和分发Debian的源代码。这使得Debian成为了一个理想的开发环境,许多Linux发行版都是基于Debian的。

Ubuntu是Debian的一个下游发行版。它采用了Debian的构建系统,然后做了很多修改,形成了自己的风格和发行方式。相较于Debian社区,Ubuntu是由Canonical公司维护的企业级产品,在稳定性和口碑上更适合生产环境使用。

Manjaro是一个基于Arch Linux的发行版。Manjaro的目标是让Arch Linux的用户能够更方便地使用Arch Linux,它提供了图形化的安装程序,以及大量的软件仓库,使得用户可以更方便地安装和管理软件。

Gentoo是一个基于Portage的发行版。Portage是一个包管理器,它允许用户从源代码编译软件,而不是从预编译的二进制文件中安装。Gentoo的目标是让用户能够完全控制他们的系统,包括选择要安装的软件包、编译选项等。
APT是Advanced Package Tool的缩写,它是一个用于处理Debian及其衍生发行版的软件包的工具。APT提供了软件包的安装、升级、删除、查询等功能,并且可以自动解决软件包之间的依赖关系。
APT的命令行工具是apt-get和apt-cache。apt-get用于安装、升级和删除软件包,apt-cache用于查询软件包的信息。
APT的配置文件位于/etc/apt/目录中,包括sources.list文件和preferences文件。sources.list文件列出了软件包的源,preferences文件用于指定软件包的优先级。
APT的软件包管理器是dpkg,它用于安装、升级和删除软件包。dpkg的配置文件位于/etc/dpkg/目录中,包括dpkg.conf文件和status文件。
APT的软件包格式是.deb,它是一个包含软件包元数据和二进制文件的文件。APT的软件包源可以是本地文件系统、HTTP服务器、FTP服务器等。
在Ubuntu中,你可以使用以下命令来更新软件源:
apt update在Ubuntu中,你可以使用以下命令来升级软件包:
apt upgradeapt install build-essential cmake git vim在内核模块开发过程中,内核模块需要调用内核本地的函数,就必须获取内核本体的符号表。然而,不同版本的内核符号表显然是不同的,因此必须在内核模块编译时,提供
完全一致版本的内核头文件。对于发行版提供的内核,获取内核头文件可以通过安装软件包的方式进行:
apt install linux-headers-$(uname -r)对于自己编译的内核,内核头文件在
make时已经生成,位于arch/x86/include/generated(或对应CPU架构)目录中。On-Policy Distillation
KL散度
反向KL散度
平滑性假设
VAE 变分=变化的分布