课程目标(45分钟)
学会 if:让程序根据不同情况,做出不同选择!
核心口诀:如果(条件)就做A,否则做B

整体风格延续
- 口语化故事教学(果果攒钱、精灵冒险)
- 比喻:如果魔法 = “十字路口的选择精灵”
- 结合前几课:变量、列表、图形、lambda
- 逐步递进:从简单判断 → 嵌套判断 → 在map里用if
- 预告下节:compose 魔法(把多个机器连成超级机器)


第一部分:问题引入(5分钟)

老师故事:
“果果攒了好多零花钱,上节课我们用 map 把所有钱都翻倍了!
现在果果要去买最新款小天才手表(2399元)!

如果钱够了 → 果果开心跳舞!🎉
如果钱不够 → 果果伤心继续攒钱 😢

电脑怎么知道‘够不够’?今天教你一个超级魔法:如果魔法!
它就像路口的选择精灵:看情况,带你走不同的路!”

板书十字路口图:

1
2
3
4
5
6
7
8
┌──────────────┐
│ 钱够吗? │
└──────┬───────┘
│
是 │ 否
┌──▼───┐ ┌──▼───┐
│ 买! │ │ 攒! │
└──────┘ └──────┘


第二部分:认识 if 魔法(15分钟)

1. 基本语法(5分钟)

1
2
3
4
5
6
7
#lang racket
(define my-money 2500) ; 果果现在的钱
(define watch-price 2399) ; 手表价格

(if (> my-money watch-price)
"可以买!开心!🎉"
"还差一点,继续攒钱😢")

运行结果:"可以买!开心!🎉"

解释: - if = 选择精灵启动 - (> my-money watch-price) = 判断条件(真或假) - 第一个字符串 = 条件为真时做的事 - 第二个字符串 = 条件为假时做的事

比喻:
if 就像一个有三个口袋的精灵:
1. 放判断问题
2. 放“对的时候做的事”
3. 放“错的时候做的事”

2. 动手练习(10分钟)

练习1:钱够不够买玩具
改数字试试不同结果!

1
2
3
4
5
6
(define my-money 2000)
(define toy-price 2200)

(if (>= my-money toy-price)
"买买买!"
"再攒攒~")

练习2:图形版如果魔法(结合第一课)

1
2
3
4
5
6
7
(require 2htdp/image)

(define mood "happy") ; 改成 "sad" 试试!

(if (string=? mood "happy")
(circle 50 "solid" "yellow") ; 开心画太阳
(circle 50 "solid" "blue")) ; 伤心画乌云

练习3:数字判断大挑战

1
2
3
4
5
6
7
(define score 85)

(if (>= score 90)
"优秀!得小红花🌸"
(if (>= score 70)
"不错!继续加油🚀"
"要努力哦~"))

老师提示:可以把 if 套 if! 这叫“嵌套判断”


第三部分:如果魔法 + 列表 + map(15分钟)

故事:
“果果有好多朋友的成绩单!我们要给每个人发不同鼓励的话!”

1
2
3
4
5
6
7
8
9
(define scores (list 95 82 68 91 75))

(map (lambda (s)
(if (>= s 90)
"超级棒!🌟"
(if (>= s 80)
"很不错!👍"
"加油哦!💪")))
scores)

结果:'("超级棒!🌟" "很不错!👍" "加油哦!💪" "超级棒!🌟" "加油哦!💪")

小挑战:价格标签升级版(结合第4课)

1
2
3
4
5
6
7
(define prices (list 10 60 35 120))

(map (lambda (p)
(if (> p 50)
(string-append (number->string p) "元(贵!)")
(string-append (number->string p) "元(便宜!)")))
prices)

结果:'("10元(便宜!)" "60元(贵!)" "35元(便宜!)" "120元(贵!)")


第四部分:综合创作(8分钟)

终极任务:果果的冒险选择器

1
2
3
4
5
6
7
8
(define energy 80)     ; 果果的体力(0-100)
(define weather "sunny") ; 天气:"sunny" 或 "rainy"

(if (>= energy 70)
(if (string=? weather "sunny")
"去公园玩!🌞"
"在家画画!🎨")
"先休息一下~😴")

创作时间:
让孩子们自己设计一个“冒险决策器”:
- 判断体力 + 天气 + 心情 → 决定今天做什么活动
- 或者:判断成绩 + 作业完成 → 决定周末玩多久游戏

老师巡回指导,鼓励多层if!


第五部分:总结与作业(2分钟)

核心口诀
if 就是选择魔法:
(if 判断题 对的时候做事 错的时候做事)

今天学会了: 1. 用 if 让程序会判断 2. 嵌套 if 做多层选择 3. 把 if 放进 map 批量判断

回家作业(简单又好玩): 1. 写一个“天气预报机”:输入天气("sunny"/"rainy"),输出今天穿什么衣服 2. 用 map + if 给 (list 45 78 92 65 88) 打等级:90+ A,80+ B,70+ C,其他 D

果果编程第5课

下节课预告
“下节课学 compose 组合魔法 —— 把多个变形机器连成一个超级大机器!
比如:先翻倍 → 再加10 → 再变字符串 → 再加‘元’……一行代码搞定!”

板书总结:

1
2
3
4
如果魔法 if
(if 条件 真→做事A 假→做事B)
可以嵌套!
可以和 map 一起用 → 批量判断!

教学小贴士
- 多用前几课内容(变量、列表、图形、map)做例子,强化衔接
- 遇到错误就说“选择精灵迷路了,我们帮他指路!”
- 进度快的孩子可以试 cond 多条件写法(预告高级魔法)

果果们,今天你们都变成了会“选择”的小魔法师啦!✨

课程目标(45分钟)

学会 map:一次性对列表中所有元素做同样的操作


第一部分:问题引入(5分钟)

老师故事: > "上节课果果攒了零花钱 (list 10 20 30 40 50)。如果每笔钱都变成2倍,怎么办?" > > 一个一个改?太累了!今天教你 数字变形工厂 —— 一个咒语,整串数字一起变!


第二部分:认识 map(15分钟)

1. 基本语法(5分钟)

1
2
3
4
5
6
#lang racket

(define savings (list 10 20 30 40 50))

; 启动变形工厂:每个数字×2
(map (lambda (n) (* n 2)) savings)

运行结果:'(20 40 60 80 100)

解释:

  • map = 变形工厂启动
  • (lambda (n) (* n 2)) = 机器说明书:"拿数字n,输出n×2"
  • savings = 原材料

2. 动手练习(10分钟)

练习1:加10机器

1
2
(map (lambda (n) (+ n 10)) savings)
; 结果:'(20 30 40 50 60)

练习2:变字符串机器

1
2
(map (lambda (n) (number->string n)) savings)
; 结果:'("10" "20" "30" "40" "50")

练习3:加"元"机器

1
2
3
4
  (map (lambda (n) 
(string-append (number->string n) "元"))
savings)
; 结果:'("10元" "20元" "30元" "40元" "50元")

小挑战:把每个数字变成它的平方

1
2
(map (lambda (n) (* n n)) savings)
; 结果:'(100 400 900 1600 2500)


第三部分:string列表变形(15分钟)

1. 名字大变身(8分钟)

1
2
3
4
5
(define names (list "果果" "小明" "小红"))

; 加"同学"
(map (lambda (name) (string-append name "同学")) names)
; 结果:'("果果同学" "小明同学" "小红同学")

练习:加"你好,"在前面

1
2
(map (lambda (name) (string-append "你好," name)) names)
; 结果:'("你好,果果" "你好,小明" "你好,小红")

2. 重复魔法(7分钟)

1
2
3
; 每个名字重复3遍
(map (lambda (name) (string-append name name name)) names)
; 结果:'("果果果果果果" "小明小明小明" "小红小红小红")

练习:名字变"名字!"

1
2
(map (lambda (name) (string-append name "!")) names)
; 结果:'("果果!" "小明!" "小红!")


第四部分:综合练习(8分钟)

挑战:制作价格标签

1
2
3
4
5
6
7
8
(define prices (list 10 25 50 100))

; 目标:'("价格:10元" "价格:25元" ...)
(map (lambda (p)
(string-append "价格:"
(number->string p)
"元"))
prices)

进阶:如果价格>50,加"(贵)"

1
2
3
4
5
6
(map (lambda (p)
(if (> p 50)
(string-append (number->string p) "元(贵)")
(string-append (number->string p) "元")))
prices)
; 结果:'("10元" "25元" "50元" "100元(贵)")


第五部分:总结(2分钟)

核心口诀

map 就是批量变形术: (map 变形机器 列表) → 新列表

回家作业

  1. 把 (list 1 2 3 4 5) 每个数字变成 "第X名"(X是数字)
  2. 把 (list "苹果" "香蕉" "橘子") 每个变成 "我爱吃XX"

都考100分哦

果果编程第4课

下节课预告

"下节课学 compose 组合魔法 —— 把多个变形机器拼成一个超级机器!"


板书:

1
2
3
4
5
6
7
8
map = 数字变形工厂
(map (lambda (x) ...) 列表)

常用变形:
(* x 2) → 翻倍
(+ x 10) → 加10
(number->string x) → 变文字
(string-append x "元") → 加单位

今天我们要学会的三件小魔法

  1. 把一堆数字串成一条“魔法数字链”(list)
  2. 从链子上“摘第一个数字”(car)和“看剩下的一串”(cdr)
  3. 请一个“不需要名字的临时小助手”帮忙算东西(lambda)

开场故事(5分钟)

老师拿着几颗糖果(或画在纸上的数字:10、20、30、40、50),串成一串:

“果果每周攒零花钱:
第一周10元,第二周20元,第三周30元……
如果我们想一次告诉电脑‘我攒了这些钱’,要怎么说呢?
一个一个写太麻烦啦!
今天我们学会用一个魔法,把它们全部串成一条‘魔法数字链’!”

第一关:学会串数字链(list)(10分钟)

在DrRacket里一起敲:

1
2
(define savings (list 10 20 30 40 50))
savings

屏幕显示:
‘(10 20 30 40 50)

老师解释:
“这个括号里的东西,就是一条数字链!
就像糖葫芦:10连着20,20连着30……
电脑一下子就记住了整串!”

再试一个:

1
2
(define scores (list 85 90 95 100))
scores

问孩子:
“你们猜猜,你们班的数学成绩如果串成一条链,会长什么样?”

小练习(让孩子自己敲):

  1. 定义一条你喜欢的数字链,比如喜欢的零食价格
    (例如:(define snacks (list 5 8 12 15)))

第二关:摘糖葫芦 —— car 和 cdr(15分钟)

老师指着糖葫芦串:
“现在我想先吃第一颗(10元),用什么魔法?”

敲代码:

1
(car savings)

答案:10

老师说:
“car 就是‘摘第一个’!
读作‘卡’——就像火车的第一节车厢。”

再敲:

1
(cdr savings)

答案:’(20 30 40 50)

老师说:
“cdr 就是‘去掉第一个,给我剩下的整串’!
读作‘抠得儿’——就像把第一颗摘掉,剩下的一串还连着。”

层层剥开演示(一起数):

1
2
3
(car (cdr savings))          ; 20
(car (cdr (cdr savings))) ; 30
(car (cdr (cdr (cdr savings)))) ; 40

白板画过程(推荐):

1
2
3
4
5
原来链子:10 → 20 → 30 → 40 → 50
car: 10
cdr: 20 → 30 → 40 → 50
再car: 20
再cdr: 30 → 40 → 50

孩子动手练习(8-10分钟):

  1. 用你刚才定义的链子,敲 (car 你的链子名字)
  2. 敲 (cdr 你的链子名字)
  3. 试试连续用 car 和 cdr,找出“第3个数字”

老师巡回:孩子卡住就一起数“第一个、第二个、第三个……”

第三关:请临时小助手 —— lambda(12分钟)

老师说:
“现在我想把这条攒钱链上的每个数字都翻倍(×2),怎么办?
一个一个写太慢!
我们可以请一个‘临时小助手’,他不需要名字,只干一件事——把数字×2。”

先简单玩玩计算:

1
2
(* 10 2)   ; 20
(* 30 2) ; 60

再介绍 lambda:

1
(lambda (n) (* n 2))

老师解释:
“这个就是临时小助手!
意思是:拿一个数字 n,把它×2。”

直接用他算:

1
2
((lambda (n) (* n 2)) 25)   ; 50
((lambda (n) (* n 2)) 7) ; 14

再试加法助手:

1
((lambda (x) (+ x 100)) 50)   ; 150

小练习(孩子敲):

  1. 请一个临时小助手,把数字+10
    试算:((lambda (y) (+ y 10)) 30) → 应该得40
  2. 请一个临时小助手,把数字平方
    试算:((lambda (z) (* z z)) 5) → 应该得25

果果编程第3课

结束总结 + 预告(3分钟)

老师一起回顾:
“今天我们学会了三招:

  1. list —— 把数字串成一条链
  2. car —— 摘第一个,cdr —— 看剩下的链
  3. lambda —— 请一个临时小助手帮忙算

下节课我们要把这些本领用在画图上!
可以一次把一串颜色变成一串气球,一串数字变成一串不同大小的星星……
准备好画生日礼物了吗?”

回家小作业(简单版)

  1. 写一条你自己的数字链(至少4个数字)
  2. 用 car 取出第一个,用 cdr 看剩下的
  3. 请一个 lambda 小助手,把某个数字×3,算一算

这个版本:

  • 每部分都只有1-2个核心代码
  • 语言超级口语化,像聊天一样
  • 完全不涉及图像,只用数字和糖葫芦比喻
  • 练习量小,成功率高

如果觉得还是太快,可以把第三关(lambda)再拆成两节课,或者把作业再简化。
需要我再调整某一部分,或者准备下一节(图形+函数+map)的课程形式,随时告诉我!

课程目标

  • 学会什么是变量(就像小盒子,可以装数字、文字)
  • 用变量存“手表价格”和“我的钱”
  • 算一算:还要攒多久才能买最新款小天才电话手表?

最新款小天才手表价格(2026年1月参考)

小天才的旗舰款目前主要是 Z11(哪吒定制/少年版)或 Z10 升级系列,官方/主流电商价格大约在 2299–2399元 左右(不同颜色/IP联名款略有浮动)。我们用 2399元 作为最新款示例(很酷的视频通话 + 精准楼层定位 + 健康检测)。

完整代码(#lang at-exp racket + infix,全部英文变量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#lang at-exp racket
(require infix)

; 第一步:定义变量(就像给小盒子贴标签,放东西进去)
(define watch_price 2399) ; 最新款小天才电话手表价格(元)
(define my_money 800) ; 我现在有的零花钱(元)
(define weekly_save 50) ; 每周能攒的钱(元)
(define weeks_needed 3)

; 第二步:用 infix 计算还要多少周(注意用 | | 包住有 - 的变量名)
@${ weeks_needed := (|watch_price| - |my_money|) / |weekly_save| }

; 直接 define 计算结果(最推荐给小朋友)
;(define weeks_needed
; (/ (- watch_price my_money) weekly_save))

; 显示结果
(display "最新小天才手表价格是: ")
(displayln watch_price)

(display "我现在有: ")
(displayln my_money)

(display "每周攒: ")
(displayln weekly_save)

(display "还要攒大约 ")
(display (ceiling weeks_needed)) ; ceiling = 向上取整,实际要买得攒够整周
(display " 周!")

; 额外:显示精确小数
(display " (精确是 ")
(display (exact->inexact weeks_needed))
(displayln " 周)")

运行后可能的输出(假设例子数字)

1
2
3
4
5
最新小天才手表价格是: 2399
我现在有: 800
每周攒: 50
还要攒大约 32 周!
(精确是 31.98 周)

解释给小朋友(上课可以说)

  • define 就像“创建一个小盒子,叫 watch_price,里面放 2399”
  • 变量名用英文(snake_case 或 camelCase 都可以),避免中文或 - 符号引起麻烦
  • := 是赋值(在 infix 里很方便)
  • ceiling 是“向上取整” → 因为你不能攒 31.98 周,必须等到第 32 周才能买哦!

练习小任务(给果果们)

  1. 把 watch_price 改成 1999(Z9 或其他款),重新跑跑看要几周?
  2. 如果每周攒 100 元,会快很多吗?改改 weekly_save 试试!
  3. 想买更贵的 2899 元联名款?改价格再算算!
1
2
3
4
5
6
7
#lang racket

(define watch_price 2299) ; 最新款小天才电话手表价格(元)
(define my_money 800) ; 我现在有的零花钱(元)
(define weekly_save 150) ; 每周能攒的钱(元)

(/ (- watch_price my_money) weekly_save)
果果编程第2课

这样第二课就很有趣啦~既有变量概念,又贴近生活(小天才手表是小学生超爱的话题)。如果想加图片或改成其他价格,随时告诉我哦!加油果果们!🚀

45分钟课程设计 | 零基础入门

课程概述

  • 主题:用代码创造魔法图形
  • 目标:让每个孩子在45分钟内画出至少3个彩色图形
  • 核心理念:编程就是创造魔法

课程准备 (5分钟)

教师准备

  1. 投影仪:展示DrRacket界面
  2. 示例代码:准备好几个有趣的图形
  3. 奖励贴纸:星星、笑脸贴纸
  4. 提前安装:确保所有电脑有DrRacket

学生准备

  1. 打开电脑,启动DrRacket
  2. 准备好”魔法小手”(手指灵活活动)
  3. 给电脑起个有趣的名字(如”魔法盒子”)

正式课程 (40分钟)

第一部分:认识魔法工具 (5分钟)

1. 趣味介绍 (2分钟)

1
2
3
"同学们好!今天我们要学习用代码创造魔法!
代码就像魔法咒语,电脑就像魔法棒,
我们一起念咒语,让电脑变出彩色图形!"

2. 认识DrRacket界面 (3分钟)

  • 上半区:”咒语书写区”(写代码的地方)
  • 下半区:”魔法展示区”(显示图形的地方)
  • 运行按钮:▶️ “魔法启动按钮”

简单演示:

1
2
3
"看老师变魔法啦!"
点击运行 → 出现红色圆圈
孩子们:"哇!"

第二部分:第一个魔法咒语 (10分钟)

1. 学习第一个图形:圆 (5分钟)

1
2
3
#lang racket
(require 2htdp/image)
(circle 50 "solid" "red")

比喻教学法:

  • circle:”变圆圈”的咒语
  • 50:圆圈的大小(像气球大小)
  • "solid":实心的(不是空心的)
  • "red":红色(魔法颜色)

学生任务:

1
2
3
1. 在咒语书写区输入这行代码
2. 点击"魔法启动按钮"(运行)
3. 看看你的第一个魔法图形!

2. 改变魔法参数 (5分钟)

小挑战:

1
2
3
"试试看,你能变出:
1. 一个蓝色的大圆圈吗?
2. 一个黄色的空心圆圈吗?"

提示:

  • 颜色可以换:"blue"、"green"、"yellow"
  • 大小可以改:30、80、100
  • 样式可以变:"outline"(空心)

第三部分:更多魔法图形 (15分钟)

1. 学习第二个图形:方形 (5分钟)

1
(square 60 "solid" "green")

学生任务:

1
2
3
4
"现在我们来变方块!"
1. 输入上面的咒语
2. 运行看看
3. 试着做一个紫色的大方块

2. 学习第三个图形:三角形 (5分钟)

1
(triangle 70 "solid" "orange")

小比赛:

1
2
3
4
5
"谁能在2分钟内变出:
1. 一个粉色三角形
2. 一个蓝色方形
3. 一个绿色圆圈
前三名获得魔法师贴纸!"

3. 图形组合游戏 (5分钟)

1
2
(beside (circle 30 "solid" "red")
(square 40 "solid" "blue"))

解释:

  • beside:让图形手拉手并排站
  • 可以组合任意两个图形

创意任务:

1
2
"创造你的第一个魔法图案:
把两个你喜欢的图形并排放!"

第四部分:小小魔法师创作 (10分钟)

1. 创作时间 (8分钟)

挑战任务:

1
2
3
4
5
"现在你是小小魔法师了!
用你学到的三个咒语,创造:
1. 一个笑脸(用圆圈和三角形)
2. 或者一座小房子(方形+三角形)
3. 或者任何你想创造的魔法图案!

教师巡回指导:

  • 帮助有困难的同学
  • 鼓励创意作品
  • 拍照记录优秀作品

2. 作品展示 (2分钟)

1
2
3
4
5
"魔法展示时间!
请三位同学分享他们的作品:
1. 你创造了什么?
2. 你用了哪些咒语?
3. 你喜欢编程魔法吗?"

课程总结与延伸 (5分钟)

1. 魔法总结 (2分钟)

1
2
3
4
5
"今天我们学会了:
✨ 三个魔法咒语:circle、square、triangle
✨ 一个组合咒语:beside
✨ 学会了控制颜色、大小和样式
你们都是很棒的小魔法师!"

2. 家庭魔法作业 (2分钟)

1
2
3
4
"回家后可以:
1. 用今天学的咒语画一个彩虹(7个不同颜色的圆圈)
2. 画一个机器人(各种形状组合)
3. 教爸爸妈妈变一个简单的魔法图形"

3. 下节课预告 (1分钟)

1
2
3
4
"下节课更有趣!
我们要让图形动起来,变成动画魔法!
还会学习星星、多边形等更多咒语!
记得带着你的魔法笔记本哦!"

教学技巧与注意事项

保持趣味性

  • 使用孩子能理解的比喻
  • 多用鼓励性语言
  • 准备小奖品(贴纸、小星星)

技术要点

  1. 括号匹配:提醒孩子括号要成对出现
  2. 引号使用:颜色要用英文双引号
  3. 大小写:Racket区分大小写

常见问题预判

  1. 忘记引号:red → "red"
  2. 括号不匹配:检查开头结尾括号
  3. 单词拼错:circl → circle

差异化教学

  • 进度快的:尝试star、ellipse等更多图形
  • 进度慢的:专注于掌握1-2个图形
  • 创意强的:鼓励设计复杂图案

板书/PPT设计

魔法咒语墙

1
2
3
4
5
魔法咒语列表:
1. (circle 大小 "solid/outline" "颜色")
2. (square 大小 "solid/outline" "颜色")
3. (triangle 大小 "solid/outline" "颜色")
4. (beside 图形1 图形2)

魔法颜色表

1
2
3
4
红色:"red"      蓝色:"blue"
绿色:"green" 黄色:"yellow"
紫色:"purple" 橙色:"orange"
粉色:"pink" 棕色:"brown"

评价标准

  • ✅ 能正确输入并运行至少1个图形
  • ✅✅ 能改变图形的颜色和大小
  • ✅✅✅ 能组合2个以上的图形
  • 🌟 有创意地设计出自己的图案

课程材料

  1. 打印的”魔法咒语卡片”(带回家复习)
  2. 学生作品展示墙(教室一角)
  3. “我是小魔法师”证书(下节课颁发)

第一课的核心目标:让孩子在第一次接触编程时,感受到”我能行”的成就感。通过简单、直观、有趣的图形编程,建立起对代码的亲近感和信心。45分钟后,每个孩子都应该带着笑容和至少一个自己创造的图形离开教室。

果果编程第一课

查看rpi日志级别

1
2
cat /proc/sys/kernel/printk
3 4 1 3

printk的四个参数详解

1
2
cat /proc/sys/kernel/printk
3 4 1 3

四个数字分别表示: 1. 控制台日志级别(3) - 只有级别≤3的消息会输出到控制台(串口/显示器) 2. 默认消息级别(4) - 没有指定级别的printk()使用这个级别 3. 最低允许级别(1) - 允许设置的最低级别 4. 启动时默认级别(3) - 内核启动早期的控制台级别

消息级别对应关系

1
2
3
4
5
6
7
8
9
级别    宏定义             说明
0 KERN_EMERG 紧急(系统可能崩溃)
1 KERN_ALERT 警报(必须立即处理)
2 KERN_CRIT 严重(严重情况)
3 KERN_ERR 错误(错误情况) ← 这是你能在控制台看到的最低级别
4 KERN_WARNING 警告
5 KERN_NOTICE 通知(普通但重要)
6 KERN_INFO 信息(普通消息) ← dev_info() 用这个级别
7 KERN_DEBUG 调试信息

对串口输出的影响

情况1:控制台日志级别=3

1
2
3
4
5
6
7
8
9
10
11
// 这些会输出到串口 ✓
pr_emerg() // 0级 ✓
pr_alert() // 1级 ✓
pr_crit() // 2级 ✓
pr_err() // 3级 ✓

// 这些不会输出到串口 ✗
pr_warn() // 4级 ✗
pr_notice() // 5级 ✗
pr_info() // 6级 ✗ - 包括 dev_info()
pr_debug() // 7级 ✗

情况2:如果设置 loglevel=7

1
2
3
4
5
6
7
8
9
// 所有消息都会输出到串口 ✓
pr_emerg() ✓
pr_alert() ✓
pr_crit() ✓
pr_err() ✓
pr_warn() ✓
pr_notice() ✓ // linux_banner 用这个级别
pr_info() ✓ // dev_info() 现在能看到了
pr_debug() ✓

加打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff --git a/init/main.c b/init/main.c
index 821df1f05e9c..05be14b81a4c 100644
--- a/init/main.c
+++ b/init/main.c
@@ -916,6 +916,11 @@ void start_kernel(void)
{
char *command_line;
char *after_dashes;
+ printk(KERN_EMERG "\n\n");
+ printk(KERN_EMERG "################################\n");
+ printk(KERN_EMERG "# RPi CUSTOM KERNEL ACTIVE #\n");
+ printk(KERN_EMERG "################################\n");
+ printk(KERN_EMERG "\n\n");

set_task_stack_end_magic(&init_task);
smp_setup_processor_id();

根据前面【树莓派学习】003-cross-compile

1
2
3
bear -- make O=build -j10 Image.gz modules dtbs
make O=build modules_install INSTALL_MOD_PATH=.
./install-rpi.sh

观察串口打印

1
2
3
4
5
6
7
8
9
10
minicom -b 115200 -D /dev/cu.usbserial-0001
[ 0.000000] ################################

[ 0.000k00] # RPi CUSTOM KERNEL ACTIVE #

[ 0.000000] ################################

[ 0.000000]

[ 0.000000]

mac pro要交叉编译树莓派linux的源码, 最好还是用ubuntu镜像。

ubuntu24.04 docker中编译, 参考dev

1
docker pull ghcr.io/cybrid-systems/dev

这里注意不要用mac mount的路径, 用docker中的文件系统路径。不然会有git工作区的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
warning: the following paths have collided (e.g. case-sensitive paths
on a case-insensitive filesystem) and only one from the same
colliding group is in the working tree:

'include/uapi/linux/netfilter/xt_CONNMARK.h'
'include/uapi/linux/netfilter/xt_connmark.h'
'include/uapi/linux/netfilter/xt_DSCP.h'
'include/uapi/linux/netfilter/xt_dscp.h'
'include/uapi/linux/netfilter/xt_MARK.h'
'include/uapi/linux/netfilter/xt_mark.h'
'include/uapi/linux/netfilter/xt_RATEEST.h'
'include/uapi/linux/netfilter/xt_rateest.h'
'include/uapi/linux/netfilter/xt_TCPMSS.h'
'include/uapi/linux/netfilter/xt_tcpmss.h'
'include/uapi/linux/netfilter_ipv4/ipt_ECN.h'
'include/uapi/linux/netfilter_ipv4/ipt_ecn.h'
'include/uapi/linux/netfilter_ipv4/ipt_TTL.h'
'include/uapi/linux/netfilter_ipv4/ipt_ttl.h'
'include/uapi/linux/netfilter_ipv6/ip6t_HL.h'
'include/uapi/linux/netfilter_ipv6/ip6t_hl.h'
'net/netfilter/xt_DSCP.c'
'net/netfilter/xt_dscp.c'
'net/netfilter/xt_HL.c'
'net/netfilter/xt_hl.c'
'net/netfilter/xt_RATEEST.c'
'net/netfilter/xt_rateest.c'
'net/netfilter/xt_TCPMSS.c'
'net/netfilter/xt_tcpmss.c'
'tools/memory-model/litmus-tests/Z6.0+pooncelock+poonceLock+pombonce.litmus'
'tools/memory-model/litmus-tests/Z6.0+pooncelock+pooncelock+pombonce.litmus'

This warning occurs on a case-insensitive filesystem (like macOS’s APFS or Windows’ NTFS) when a Git repository contains files whose names differ only in case. Git detects these as potential conflicts because the filesystem treats them as identical.

1
2
3
git clone --depth=1 https://github.com/raspberrypi/linux.git
cd linux
apt install bc
1
2
3
4
mkdir build
make O=build bcm2711_defconfig
bear -- make O=build -j10 Image.gz modules dtbs
make O=build modules_install INSTALL_MOD_PATH=.

debug config

1
2
3
4
5
6
7
8
9
10
11
12
cat >> build/.config << 'EOF'
# Early Debug Configuration
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
CONFIG_EARLY_PRINTK=y
CONFIG_DEBUG_LL=y
CONFIG_DEBUG_UART_PL011=y
CONFIG_DEBUG_UART_PHYS=0x3f215040
CONFIG_DEBUG_UART_VIRT=0xf215040
CONFIG_DEBUG_UNCOMPRESS=y
CONFIG_DEBUG_LL_INCLUDE="debug/8250.S"
EOF

~/.config/clangd/config.yaml

1
2
CompileFlags:
Remove: [-mabi=lp64]

install-rpi.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 🎯 完整传输(rsync + sudo,权限OK)

KVER=$(ls build/lib/modules/ | head -1)
echo "模块版本: $KVER"

# 1. Image.gz(压缩内核,必须)
rsync -avz build/arch/arm64/boot/Image.gz pi@192.168.1.95:/boot/firmware/ --rsync-path="sudo rsync"

# 2. DTB(设备树,bcm2711/rpi5)
rsync -avz build/arch/arm64/boot/dts/broadcom/*.dtb pi@192.168.1.95:/boot/firmware/ --rsync-path="sudo rsync"

# 3. Overlays(驱动叠加,必须完整目录)
rsync -avz --delete build/arch/arm64/boot/dts/overlays/ pi@192.168.1.95:/boot/firmware/overlays/ --rsync-path="sudo rsync"

# 4. Modules(驱动模块,关键!)
rsync -avz --delete build/lib/modules/$KVER/ pi@192.168.1.95:/lib/modules/$KVER/ --rsync-path="sudo rsync"
ssh pi@192.168.1.95 "sudo depmod -a $KVER && echo 'depmod完成'"

# 5. 更新 + 重启
ssh pi@192.168.1.95 'cd ~/code/kernel-update && ./update-kernel.sh && sudo reboot'

~/code/kernel-update/backup-kernel.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
# 保存到: ~/code/kernel-update/backup-kernel.sh

echo "=== 开始备份当前内核和配置 ==="

# 创建备份目录
sudo mkdir -p /boot/firmware/backup
BACKUP_DIR="/boot/firmware/backup/$(date +%Y%m%d_%H%M%S)"
sudo mkdir -p "$BACKUP_DIR"

# 备份内核文件
echo "备份内核文件..."
sudo cp /boot/firmware/kernel8.img "$BACKUP_DIR/"
sudo cp /boot/firmware/Image.gz "$BACKUP_DIR/" 2>/dev/null || true

# 备份配置文件
echo "备份配置文件..."
sudo cp /boot/firmware/config.txt "$BACKUP_DIR/"
sudo cp /boot/firmware/cmdline.txt "$BACKUP_DIR/"

# 备份设备树
echo "备份设备树..."
sudo cp /boot/firmware/*.dtb "$BACKUP_DIR/" 2>/dev/null || true
sudo cp -r /boot/firmware/overlays "$BACKUP_DIR/" 2>/dev/null || true

# 备份网络配置(重要!)
echo "备份网络配置..."
sudo cp -r /etc/netplan "$BACKUP_DIR/" 2>/dev/null || true
sudo cp -r /etc/NetworkManager "$BACKUP_DIR/" 2>/dev/null || true

echo "=== 备份完成 ==="
echo "备份位置: $BACKUP_DIR"
echo "当前备份列表:"
sudo ls -la "$BACKUP_DIR"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash
# 保存到: ~/code/kernel-update/update-kernel.sh

echo "=== 开始更新内核 ==="

# 检查新内核文件是否存在
if [ ! -f "/boot/firmware/Image.gz" ]; then
echo "错误: 未找到新内核文件 /boot/firmware/Image.gz"
echo "请先传输新内核文件"
exit 1
fi

# 备份当前配置
./backup-kernel.sh

# 配置使用新内核
echo "配置使用新内核..."
sudo cp /boot/firmware/config.txt /boot/firmware/config.txt.backup

# 在config.txt中启用新内核
if grep -q "kernel=Image.gz" /boot/firmware/config.txt; then
echo "新内核已配置"
else
# 注释原内核,启用新内核
sudo sed -i 's/^kernel=/#kernel=/' /boot/firmware/config.txt
echo "kernel=Image.gz" | sudo tee -a /boot/firmware/config.txt
fi

echo "=== 内核更新配置完成 ==="
echo "重启后生效: sudo reboot"
echo "验证命令: uname -r"

验证

1
2
3
4
5
6
7
8
9
10
11
12
% ssh pi@pi                                                                                             yuanqi@MacBook-Pro-2
Linux pi 6.12.57-v8+ #1 SMP PREEMPT Fri Nov 14 20:56:44 CST 2025 aarch64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Dec 7 14:02:19 2025 from 192.168.1.100
pi@pi:~ $ uname -r
6.12.57-v8+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash
# 保存到: ~/code/kernel-update/restore-kernel.sh

echo "=== 内核恢复工具 ==="

# 查找最新的备份
LATEST_BACKUP=$(sudo ls -td /boot/firmware/backup/*/ | head -1)

if [ -z "$LATEST_BACKUP" ]; then
echo "错误: 未找到备份文件"
exit 1
fi

echo "找到备份: $LATEST_BACKUP"
echo "恢复内容:"
sudo ls -la "$LATEST_BACKUP"

read -p "确认恢复?(y/n): " confirm
if [ "$confirm" != "y" ]; then
echo "取消恢复"
exit 0
fi

# 恢复内核文件
echo "恢复内核文件..."
sudo cp "$LATEST_BACKUP/kernel8.img" /boot/firmware/ 2>/dev/null || true
sudo cp "$LATEST_BACKUP/Image.gz" /boot/firmware/ 2>/dev/null || true

# 恢复配置文件
echo "恢复配置文件..."
sudo cp "$LATEST_BACKUP/config.txt" /boot/firmware/
sudo cp "$LATEST_BACKUP/cmdline.txt" /boot/firmware/

# 恢复设备树
echo "恢复设备树..."
sudo cp "$LATEST_BACKUP"/*.dtb /boot/firmware/ 2>/dev/null || true
sudo cp -r "$LATEST_BACKUP/overlays" /boot/firmware/ 2>/dev/null || true

# 恢复网络配置(如果需要)
if [ -d "$LATEST_BACKUP/netplan" ]; then
echo "恢复网络配置..."
sudo cp -r "$LATEST_BACKUP/netplan" /etc/
sudo netplan apply
fi

echo "=== 恢复完成 ==="
echo "请重启: sudo reboot"

使用 ftrace 观测 GPIO 从用户调用到驱动的完整轨迹需要跟踪系统调用、VFS 层、设备驱动等多个层次。以下是详细的观测方案:

1. 准备环境和测试程序

1.1 简单的 GPIO 测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// trace_gpio_test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

int main() {
int fd;
void *gpio_map;

printf("GPIO mmap 测试开始...\n");

// 打开 gpiomem 设备
fd = open("/dev/gpiomem", O_RDWR | O_SYNC);
if (fd < 0) {
perror("open /dev/gpiomem failed");
return -1;
}
printf("GPIO设备打开成功: fd=%d\n", fd);

// 执行 mmap
gpio_map = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (gpio_map == MAP_FAILED) {
perror("mmap failed");
close(fd);
return -1;
}
printf("GPIO映射成功: %p\n", gpio_map);

// 简单的 GPIO 访问测试
volatile uint32_t *gpio = (volatile uint32_t*)gpio_map;
uint32_t value = gpio[0]; // 读取第一个寄存器
printf("GPIO寄存器值: 0x%08x\n", value);

// 保持一段时间以便观察
printf("按回车键继续...\n");
getchar();

// 清理
munmap(gpio_map, 4096);
close(fd);
printf("测试完成\n");

return 0;
}

编译运行:

1
2
3
cd ~/code
gcc trace_gpio_test.c -o trace_gpio
./trace_gpio

2. 配置 ftrace 跟踪点

2.1 实际可用的跟踪脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/bin/bash
# trace_gpio_mmap.sh

echo "=== GPIO mmap 完整轨迹跟踪 ==="

# 挂载 debugfs
mount -t debugfs nodev /sys/kernel/debug/tracing 2>/dev/null
cd /sys/kernel/debug/tracing

# 1. 重置跟踪器
echo 0 | sudo tee tracing_on > /dev/null
echo | sudo tee trace > /dev/null
echo | sudo tee set_ftrace_filter > /dev/null
echo | sudo tee set_graph_function > /dev/null

# 2. 设置 function_graph 跟踪器
echo function_graph | sudo tee current_tracer > /dev/null

# 3. 设置已验证的函数名(根据 available_filter_functions)
echo "__arm64_sys_mmap" | sudo tee -a set_graph_function > /dev/null
echo "ksys_mmap_pgoff" | sudo tee -a set_graph_function > /dev/null
echo "vm_mmap_pgoff" | sudo tee -a set_graph_function > /dev/null
echo "do_mmap" | sudo tee -a set_graph_function > /dev/null
echo "mmap_region" | sudo tee -a set_graph_function > /dev/null
echo "__mmap_region" | sudo tee -a set_graph_function > /dev/null
echo "rpi_gpiomem_mmap" | sudo tee -a set_graph_function > /dev/null
echo "rpi_gpiomem_open" | sudo tee -a set_graph_function > /dev/null

# 4. 添加 VMA 相关函数
echo "vm_area_alloc" | sudo tee -a set_graph_function > /dev/null
echo "vma_merge_new_range" | sudo tee -a set_graph_function > /dev/null
echo "vma_link_file" | sudo tee -a set_graph_function > /dev/null
echo "vma_set_page_prot" | sudo tee -a set_graph_function > /dev/null

# 5. 添加内存映射核心函数
echo "remap_pfn_range" | sudo tee -a set_graph_function > /dev/null
echo "remap_pfn_range_internal" | sudo tee -a set_graph_function > /dev/null
echo "__pte_offset_map_lock" | sudo tee -a set_graph_function > /dev/null

# 6. 配置跟踪选项
echo 1 | sudo tee options/func_stack_trace > /dev/null
echo 1 | sudo tee options/sleep-time > /dev/null
echo 1 | sudo tee options/graph-time > /dev/null

# 7. 开始跟踪
echo 1 | sudo tee tracing_on > /dev/null

# 8. 运行测试程序
echo "运行测试程序..."
/home/pi/code/trace_gpio &

# 9. 等待并停止跟踪
sleep 3
echo 0 | sudo tee tracing_on > /dev/null

# 10. 保存跟踪结果
sudo cat trace > /tmp/gpio_mmap_complete_trace.log
echo "跟踪结果保存到: /tmp/gpio_mmap_complete_trace.log"

# 11. 显示关键部分
echo "=== GPIO 驱动调用路径 ==="
sudo cat trace | grep -A30 -B5 "rpi_gpiomem" | head -80

3. 详细的函数跟踪配置

3.1 完整的已验证函数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/bin/bash
# trace_gpio_detailed.sh

cd /sys/kernel/debug/tracing

# 清除之前的设置
echo 0 | sudo tee tracing_on > /dev/null
echo nop | sudo tee current_tracer > /dev/null
echo | sudo tee set_ftrace_filter > /dev/null
echo | sudo tee set_graph_function > /dev/null

# 设置 function_graph
echo function_graph | sudo tee current_tracer > /dev/null

# 系统调用入口
echo "__arm64_sys_mmap" | sudo tee -a set_graph_function > /dev/null
echo "ksys_mmap_pgoff" | sudo tee -a set_graph_function > /dev/null

# 内存管理核心函数
echo "vm_mmap_pgoff" | sudo tee -a set_graph_function > /dev/null
echo "do_mmap" | sudo tee -a set_graph_function > /dev/null
echo "mmap_region" | sudo tee -a set_graph_function > /dev/null
echo "__mmap_region" | sudo tee -a set_graph_function > /dev/null

# VMA 操作函数
echo "vm_area_alloc" | sudo tee -a set_graph_function > /dev/null
echo "vma_merge_new_range" | sudo tee -a set_graph_function > /dev/null
echo "vma_link_file" | sudo tee -a set_graph_function > /dev/null
echo "vma_set_page_prot" | sudo tee -a set_graph_function > /dev/null

# 文件操作相关
echo "fget" | sudo tee -a set_graph_function > /dev/null
echo "fput" | sudo tee -a set_graph_function > /dev/null

# 驱动特定函数
echo "rpi_gpiomem_mmap" | sudo tee -a set_graph_function > /dev/null
echo "rpi_gpiomem_open" | sudo tee -a set_graph_function > /dev/null

# 页表操作函数
echo "remap_pfn_range" | sudo tee -a set_graph_function > /dev/null
echo "remap_pfn_range_internal" | sudo tee -a set_graph_function > /dev/null
echo "__pte_offset_map_lock" | sudo tee -a set_graph_function > /dev/null

# 安全相关函数
echo "security_mmap_file" | sudo tee -a set_graph_function > /dev/null
echo "security_mmap_addr" | sudo tee -a set_graph_function > /dev/null
echo "cap_mmap_file" | sudo tee -a set_graph_function > /dev/null
echo "cap_mmap_addr" | sudo tee -a set_graph_function > /dev/null

# 设置跟踪选项
echo 1 | sudo tee options/func_stack_trace > /dev/null
echo 1 | sudo tee options/graph-time > /dev/null

# 开始跟踪
echo 1 | sudo tee tracing_on > /dev/null
echo "跟踪开始,运行测试程序..."
/home/pi/code/trace_gpio &
sleep 3
echo 0 | sudo tee tracing_on > /dev/null

# 显示结果
sudo cat trace | head -100

4. 使用 trace-cmd 进行更精细控制

4.1 trace-cmd 配置(针对 GPIO)

1
2
3
4
5
6
# 安装 trace-cmd
sudo apt update
sudo apt install trace-cmd

# 验证安装
trace-cmd --version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
# trace_gpio_tracecmd.sh

echo "使用 trace-cmd 跟踪 GPIO mmap..."

# 记录跟踪数据(使用 sudo)
timeout 10 sudo trace-cmd record \
-e syscalls:sys_enter_mmap \
-e syscalls:sys_exit_mmap \
-p function_graph \
-g __arm64_sys_mmap \
-g rpi_gpiomem_mmap \
-g remap_pfn_range \
-F /home/pi/code/trace_gpio

# 生成报告(旧版本命令)
echo "=== GPIO 相关事件 ==="
sudo trace-cmd report | grep -i gpio

echo -e "\n=== mmap 系统调用 ==="
sudo trace-cmd report | grep -i mmap

echo -e "\n=== 函数图分析 ==="
sudo trace-cmd report | grep -A10 -B5 "rpi_gpiomem_mmap\|__arm64_sys_mmap"

或者使用更简单的方法:

1
2
sudo trace-cmd record -e 'syscalls:*mmap*' -p function -F /home/pi/code/trace_gpio
sudo trace-cmd report

5. 分析 ftrace 输出日志

5.1 关键路径识别方法

根据 /tmp/gpio_mmap_complete_trace.log 分析 GPIO mmap 完整轨迹:

1. 识别系统调用入口

1
2
# 查找 mmap 系统调用开始
grep -n "__arm64_sys_mmap\|ksys_mmap" /tmp/gpio_mmap_complete_trace.log

2. 跟踪内存管理调用链

1
2
# 查看内存管理核心函数
grep -A10 -B5 "do_mmap\|mmap_region\|__mmap_region" /tmp/gpio_mmap_complete_trace.log

3. 定位驱动特定函数

1
2
# 查找 GPIO 驱动函数
grep -A20 -B10 "rpi_gpiomem_mmap\|rpi_gpiomem_open" /tmp/gpio_mmap_complete_trace.log

5.2 性能热点分析

分析页表操作频率:

1
2
3
# 统计 __pte_offset_map_lock 调用次数和总耗时
grep "__pte_offset_map_lock" /tmp/gpio_mmap_complete_trace.log | wc -l
grep "__pte_offset_map_lock" /tmp/gpio_mmap_complete_trace.log | awk '{sum += $3} END {print "总耗时:", sum, "us"}'

识别内存分配瓶颈:

1
2
# 查看 VMA 分配耗时
grep -A5 "vm_area_alloc\|kmem_cache_alloc" /tmp/gpio_mmap_complete_trace.log

5.3 调用深度分析

重建完整调用路径:

1
2
# 提取关键函数的时间线
awk '/__arm64_sys_mmap/,/rpi_gpiomem_mmap/' /tmp/gpio_mmap_complete_trace.log | head -50

验证驱动映射成功:

1
2
# 检查 remap_pfn_range 是否执行
grep -A10 "remap_pfn_range" /tmp/gpio_mmap_complete_trace.log
## mmap分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
tail -100 /tmp/gpio_mmap_complete_trace.log
3) 0.593 us | _raw_spin_lock();
3) 3.037 us | }
3) | __pte_offset_map_lock() {
3) 0.685 us | __rcu_read_lock();
3) 0.685 us | _raw_spin_lock();
3) 3.019 us | }
3) | __arm64_sys_mmap() {
3) | ksys_mmap_pgoff() {
3) | fget() {
3) 0.574 us | __rcu_read_lock();
3) 0.667 us | __rcu_read_unlock();
3) 3.056 us | }
3) | vm_mmap_pgoff() {
3) | security_mmap_file() {
3) 0.556 us | cap_mmap_file();
3) 1.870 us | }
3) 0.722 us | down_write_killable();
3) | do_mmap() {
3) | __get_unmapped_area() {
3) | mm_get_unmapped_area_vmflags() {
3) | generic_get_unmapped_area_topdown() {
3) 1.352 us | vm_unmapped_area();
3) 2.574 us | }
3) 3.889 us | }
3) | security_mmap_addr() {
3) 0.593 us | cap_mmap_addr();
3) 1.852 us | }
3) 7.463 us | }
3) | memfd_file_seals_ptr() {
3) 0.556 us | shmem_mapping();
3) 1.834 us | }
3) 0.685 us | path_noexec();
3) | mmap_region() {
3) | __mmap_region() {
3) 0.666 us | may_expand_vm();
3) 0.648 us | vms_clean_up_area();
3) | vma_merge_new_range() {
3) 0.592 us | can_vma_merge_right();
3) 1.870 us | }
3) | vm_area_alloc() {
3) 0.667 us | kmem_cache_alloc_noprof();
3) 0.685 us | kmem_cache_alloc_noprof();
3) 0.574 us | __init_rwsem();
3) 4.463 us | }
3) 0.574 us | vm_get_page_prot();
3) 0.629 us | kmem_cache_alloc_noprof();
3) | ext4_file_mmap() {
3) | touch_atime() {
3) | atime_needs_update() {
3) 0.574 us | make_vfsuid();
3) 0.555 us | make_vfsgid();
3) 3.000 us | }
3) 4.297 us | }
3) 5.408 us | }
3) 0.611 us | down_write();
3) 0.703 us | up_write();
3) | call_rcu() {
3) | __call_rcu_common.constprop.0() {
3) 0.574 us | rcu_segcblist_enqueue();
3) 1.870 us | }
3) 3.186 us | }
3) | vma_link_file() {
3) 0.611 us | down_write();
3) 0.796 us | vma_interval_tree_insert();
3) 0.593 us | up_write();
3) 4.481 us | }
3) 0.574 us | perf_event_mmap();
3) 0.574 us | vms_complete_munmap_vmas();
3) | vma_set_page_prot() {
3) 0.667 us | vm_get_page_prot();
3) 0.648 us | vma_wants_writenotify();
3) 2.907 us | }
3) + 37.925 us | }
3) + 39.185 us | }
3) + 52.093 us | }
3) 0.574 us | up_write();
3) + 58.315 us | }
3) 0.574 us | fput();
3) + 65.111 us | }
3) + 66.555 us | }
3) | __pte_offset_map_lock() {
3) 0.667 us | __rcu_read_lock();
3) 0.686 us | _raw_spin_lock();
3) 2.982 us | }
3) | __pte_offset_map_lock() {
3) 0.685 us | __rcu_read_lock();
3) 0.666 us | _raw_spin_lock();
3) 2.963 us | }
3) | __pte_offset_map_lock() {
3) 0.574 us | __rcu_read_lock();
3) 0.593 us | _raw_spin_lock();
3) 3.074 us | }
3) | __pte_offset_map_lock() {
3) 0.575 us | __rcu_read_lock();
3) 0.592 us | _raw_spin_lock();
3) 3.093 us | }
3) | __pte_offset_map_lock() {
3) 0.575 us | __rcu_read_lock();
3) 0.574 us | _raw_spin_lock();
3) 2.963 us | }

当用户调用mmap()的时候,内核会进行如下处理 1. 在进程的虚拟空间查找一块VMA 2. 将这块VMA进行映射 3. 如果设备驱动程序或者文件系统的file_operations定义了mmap()操作,则调用它。 4. 将这个VMA插入进程的VMA链表中

从 ftrace 输出可以清晰地验证 mmap 处理的四个步骤:

1. 查找 VMA (虚拟内存区域)

1
2
3
4
5
6
7
8
9
# 在进程虚拟空间查找空闲区域
__get_unmapped_area() {
mm_get_unmapped_area_vmflags() {
generic_get_unmapped_area_topdown() {
vm_unmapped_area(); # 查找合适的虚拟地址范围
}
}
}
security_mmap_addr(); # 安全检查

2. 分配和设置 VMA

1
2
3
4
5
6
7
8
vm_area_alloc() {  # 分配 VMA 结构体
kmem_cache_alloc_noprof(); # 从 slab 分配器分配内存
}
vm_get_page_prot(); # 设置页面保护权限
vma_set_page_prot() {
vm_get_page_prot(); # 获取页面保护标志
vma_wants_writenotify(); # 设置写通知
}

3. 调用驱动/文件系统的 mmap 操作

1
2
3
4
5
6
7
8
9
ext4_file_mmap() {  # 文件系统的 mmap 操作
touch_atime() { # 更新访问时间
atime_needs_update() {
make_vfsuid(); # 权限检查
make_vfsgid();
}
}
}
# 注意:这里调用的是 ext4 的 mmap,如果是 GPIO 驱动应该显示 rpi_gpiomem_mmap

4. 将 VMA 插入进程链表

1
2
3
4
5
vma_link_file() {  # 将 VMA 链接到进程的 VMA 链表
down_write(); # 获取写锁
vma_interval_tree_insert(); # 插入到区间树中
up_write(); # 释放锁
}

关键证据总结:

  1. VMA 查找:__get_unmapped_area() 和 vm_unmapped_area() 负责在进程地址空间找到合适区域
  2. VMA 分配:vm_area_alloc() 明确分配了 VMA 结构体
  3. 驱动 mmap 调用:ext4_file_mmap() 显示了文件操作的具体 mmap 实现(在你的 GPIO 案例中应该是 rpi_gpiomem_mmap)
  4. VMA 链表插入:vma_link_file() 和 vma_interval_tree_insert() 完成了 VMA 到进程数据结构的链接

这个 trace 完美印证了 mmap 的四个处理步骤。

1. 在进程虚拟空间查找 VMA

源码路径: mm/mmap.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* mm/mmap.c */
unsigned long __get_unmapped_area(struct file *file, unsigned long addr,
unsigned long len, unsigned long pgoff, unsigned long flags)
{
if (flags & MAP_FIXED)
return addr;

// 根据布局策略查找空闲区域
if (addr)
return mm_get_unmapped_area_vmflags(file, addr, len, pgoff, flags);
else
return mm_get_unmapped_area(file, addr, len, pgoff, flags);
}

unsigned long mm_get_unmapped_area(struct file *file, unsigned long addr,
unsigned long len, unsigned long pgoff, unsigned long flags)
{
// 调用架构特定的查找函数
if (current->mm->get_unmapped_area)
return current->mm->get_unmapped_area(file, addr, len, pgoff, flags);

// 默认实现:从高地址向下查找
return generic_get_unmapped_area_topdown(file, addr, len, pgoff, flags);
}

关键函数调用链:

1
2
3
4
5
__arm64_sys_mmap
→ ksys_mmap_pgoff
→ vm_mmap_pgoff
→ do_mmap
→ __get_unmapped_area // 步骤1:查找空闲VMA区域

2. 分配和设置 VMA

源码路径: mm/mmap.c - mmap_region() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* mm/mmap.c */
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;

// 步骤2.1:分配 VMA 结构体
vma = vm_area_alloc(mm);
if (!vma)
return -ENOMEM;

vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;

if (file) {
vma->vm_file = get_file(file);
// 步骤2.2:调用文件/驱动的 mmap 操作
error = call_mmap(file, vma);
if (error)
goto unmap_and_free_vma;
}

// 步骤2.3:设置页面保护
vma_set_page_prot(vma);
}

VMA 分配细节:

1
2
3
4
5
6
7
8
9
10
11
/* mm/vmalloc.c */
struct vm_area_struct *vm_area_alloc(struct mm_struct *mm)
{
// 从 slab 分配器分配 VMA 结构体
struct vm_area_struct *vma = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
if (vma) {
vma_init(vma, mm); // 初始化 VMA
vma->vm_rb = RB_CLEAR_NODE;
}
return vma;
}

3. 调用驱动/文件系统的 mmap 操作

源码路径: include/linux/fs.h

1
2
3
4
5
6
/* include/linux/fs.h */
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
// 调用文件操作的 mmap 函数
return file->f_op->mmap(file, vma);
}

对于 GPIO 驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* drivers/char/broadcom/bcm2835-gpiomem.c */
static int bcm2835_gpiomem_mmap(struct file *file, struct vm_area_struct *vma)
{
struct bcm2835_gpiomem_dev *dev = file->private_data;

// 设置 VMA 标志
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

// 关键:建立物理地址到虚拟地址的映射
return remap_pfn_range(vma,
vma->vm_start,
dev->phys_addr >> PAGE_SHIFT, // 物理页帧号
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}

remap_pfn_range 内部实现:

1
2
3
4
5
6
7
/* mm/memory.c */
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot)
{
// 建立页表映射
return remap_pfn_range_internal(vma, addr, pfn, size, prot);
}

4. 将 VMA 插入进程 VMA 链表

源码路径: mm/mmap.c - __vma_link() 和 __vma_link_file()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* mm/mmap.c */
static void __vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, struct rb_node **rb_link,
struct rb_node *rb_parent)
{
// 插入到红黑树中(用于快速查找)
__vma_link_rb(mm, vma, rb_link, rb_parent);

// 插入到双向链表中(用于顺序遍历)
__vma_link_list(mm, vma, prev);
}

static void __vma_link_file(struct vm_area_struct *vma)
{
struct file *file = vma->vm_file;

if (file) {
struct address_space *mapping = file->f_mapping;

// 将 VMA 链接到文件的地址空间
vma_interval_tree_insert(vma, &mapping->i_mmap);

// 更新统计信息
flush_dcache_mmap_lock(mapping);
mapping->i_mmap_writable++;
flush_dcache_mmap_unlock(mapping);
}
}

完整的 mmap_region 流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* mm/mmap.c */
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
// ... 前面的 VMA 分配和 mmap 调用

// 步骤4:将 VMA 插入进程数据结构
vma_link(mm, vma, prev, rb_link, rb_parent);

// 如果是文件映射,链接到文件的 VMA 树
if (file)
__vma_link_file(vma);

// 通知内存管理子系统
perf_event_mmap(vma);

return addr;
}

完整的调用链总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用户空间: mmap() 系统调用
↓
__arm64_sys_mmap (arch/arm64/kernel/sys.c)
↓
ksys_mmap_pgoff (mm/mmap.c)
↓
vm_mmap_pgoff (mm/mmap.c)
↓
do_mmap (mm/mmap.c)
↓
__get_unmapped_area() // 步骤1:查找空闲VMA区域
↓
mmap_region (mm/mmap.c)
↓
vm_area_alloc() // 步骤2:分配VMA结构体
↓
call_mmap() → file->f_op->mmap() // 步骤3:调用驱动mmap操作
↓
remap_pfn_range() // 建立页表映射
↓
vma_link() // 步骤4:插入VMA链表
↓
__vma_link_rb() // 插入红黑树
↓
__vma_link_list() // 插入双向链表

这个流程清晰地展示了 Linux 内核如何处理 mmap 请求:从虚拟地址空间的查找,到 VMA 结构的分配和设置,再到驱动特定映射操作的执行,最后将 VMA 整合到进程的内存管理数据结构中。

实验名称:树莓派PWM信号生成与示波器分析

实验目标

  1. 通过Python编程,让树莓派生成一个可调占空比的PWM信号。
  2. 用示波器捕捉并测量该PWM信号的频率和占空比,验证软件控制与硬件输出的一致性。

所需设备

  1. 硬件:
    • 树莓派 (任何型号,已安装系统)
    • MacBook
    • 数字示波器
    • 面包板、LED灯、220Ω电阻、杜邦线若干
    • USB转TTL串口模块 (可选,用于串口调试)
  2. 软件:
    • MacBook上的终端 (用于SSH或串口连接)
    • 树莓派上的Python3及 RPi.GPIO 库

具体步骤

第一步:硬件连接

  1. 树莓派GPIO连接:
    • 将LED的正极(长脚)通过一个220Ω的限流电阻,连接到树莓派的 GPIO 18 (物理引脚12)。这是因为GPIO 18支持硬件PWM。
    • 将LED的负极(短脚)连接到树莓派的 GND (物理引脚6)。
  2. 示波器连接:
    • 将示波器探头的尖端钩在 GPIO 18 引脚上。
    • 将示波器探头的接地夹夹在树莓派的 GND 引脚上。
  3. MacBook连接树莓派:
    • 方法A (推荐,使用SSH):确保MacBook和树莓派在同一个Wi-Fi下,在Mac终端输入:ssh pi@<你的树莓派IP地址>,然后输入密码。
    • 方法B (使用串口):通过USB转TTL模块连接树莓派的UART引脚,在Mac上用 screen 命令连接。

连接示意图:

1
2
3
树莓派 GPIO 18 ---[220Ω电阻]--- LED(+) --- LED(-) --- GND
|
+--- (连接至示波器探头)

1. 命令行工具:pinout

这是最直接、最推荐的方法。

在你的树莓派终端中,直接输入:

1
pinout
这个命令会返回一个清晰的ASCII艺术图,明确标出了: - 物理引脚编号 (1, 2, 3...) - BCM GPIO编号 (GPIO 2, GPIO 3...),这是 RPi.GPIO 库默认使用的编号。 - 电源引脚 (3.3V, 5V, GND) - 特殊功能引脚 (例如,我们实验要用到的GPIO 18)

示例输出(部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
   3V3  (1) ◉◉ (2)  5V
GPIO2 (3) ◉◉ (4) 5V
GPIO3 (5) ◉◉ (6) GND
GPIO4 (7) ◉◉ (8) GPIO14
GND (9) ◉◉ (10) GPIO15
GPIO17 (11) ◉◉ (12) GPIO18 <-- 我们要用的引脚!
GPIO27 (13) ◉◉ (14) GND
GPIO22 (15) ◉◉ (16) GPIO23
3V3 (17) ◉◉ (18) GPIO24
GPIO10 (19) ◉◉ (20) GND
GPIO9 (21) ◉◉ (22) GPIO25
GPIO11 (23) ◉◉ (24) GPIO8
GND (25) ◉◉ (26) GPIO7

从图中可以看到,物理引脚12 对应的就是 BCM GPIO 18。

2. GPIO库的Python命令

如果你已经在Python环境中,可以这样验证:

  1. 在树莓派终端输入 python3 进入Python交互环境。
  2. 依次输入以下命令:
    1
    2
    3
    4
    5
    import RPi.GPIO as GPIO
    GPIO.setmode(GPIO.BCM) # 设置为BCM编号模式
    # 将GPIO 18设置为输出模式,这会初始化该引脚
    GPIO.setup(18, GPIO.OUT)
    print("GPIO 18 设置成功!")
    如果没有报错,说明GPIO 18是可用的。

第二步:创建并运行Python脚本

  1. 在树莓派上,创建一个Python文件,例如 pwm_test.py:

    1
    vi pwm_test.py

  2. 将以下代码复制进去:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    #!/usr/bin/env python3
    import RPi.GPIO as GPIO
    import time
    import atexit

    PWM_FREQUENCY = 1000 # 频率
    PWM_DUTY_CYCLE = 50 # 占空比
    # 全局变量跟踪PWM状态
    pwm_active = False
    pwm_instance = None

    def cleanup_resources():
    """在程序退出时清理资源"""
    global pwm_active, pwm_instance
    if pwm_active and pwm_instance:
    try:
    pwm_instance.stop()
    pwm_active = False
    pwm_instance = None
    except:
    pass
    try:
    GPIO.cleanup()
    except:
    pass

    # 注册退出处理函数
    atexit.register(cleanup_resources)

    try:
    # 设置GPIO
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(18, GPIO.OUT)

    # 创建并启动PWM
    pwm_instance = GPIO.PWM(18, PWM_FREQUENCY)
    pwm_instance.start(PWM_DUTY_CYCLE)
    pwm_active = True

    print(f"PWM已启动,频率{PWM_FREQUENCY}Hz,占空比{PWM_DUTY_CYCLE}%。按 Ctrl+C 停止程序。")

    # 主循环
    while True:
    time.sleep(1)

    except KeyboardInterrupt:
    print("\n程序被用户中断")

    finally:
    cleanup_resources()
    print("资源清理完成")

  3. 保存并退出 (在nano中按 Ctrl+X, 然后按 Y, 最后按 Enter)。

  4. 运行脚本 (需要sudo权限来访问GPIO):

    1
    sudo python3 pwm_test.py
    ### 硬件PWM

方法1:从源码编译安装pigpio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装编译依赖
sudo apt update
sudo apt install python3-pigpio

# 下载并编译pigpio
cd ~
wget https://github.com/joan2937/pigpio/archive/refs/heads/master.zip
unzip master.zip
cd pigpio-master
make -j
sudo make install

# 启动守护进程
sudo pigpiod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python3
import pigpio
import time
import signal
import sys

# 硬件PWM引脚(仅GPIO12, GPIO13, GPIO18, GPIO19支持硬件PWM)
PWM_PIN = 18
FREQUENCY = 1000000 # 1MHz
DUTY_CYCLE = 500000 # 50% (范围: 0-1000000)

pi = pigpio.pi()

def cleanup(signum=None, frame=None):
"""清理资源"""
pi.set_mode(PWM_PIN, pigpio.INPUT)
pi.stop()
print("\n资源清理完成")
sys.exit(0)

# 注册信号处理
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)

try:
# 设置硬件PWM
pi.set_mode(PWM_PIN, pigpio.OUTPUT)
pi.hardware_PWM(PWM_PIN, FREQUENCY, DUTY_CYCLE)

print(f"硬件PWM已启动: GPIO{PWM_PIN}, {FREQUENCY}Hz, 50%占空比")
print("按 Ctrl+C 停止")

while True:
time.sleep(1)

except Exception as e:
print(f"错误: {e}")
cleanup()

**方法2:gpiozero

1
2
# 安装gpiozero(通常已预装)
sudo apt install python3-gpiozero
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python3
from gpiozero import PWMOutputDevice
import time
import signal
import sys

# 使用GPIO18(硬件PWM引脚)
pwm = PWMOutputDevice(18, frequency=1000)

def cleanup(signum, frame):
pwm.close()
print("\n资源清理完成")
sys.exit(0)

signal.signal(signal.SIGINT, cleanup)

try:
pwm.value = 0.5 # 50%占空比
print("硬件PWM已启动: 1kHz, 50%占空比")
print("按 Ctrl+C 停止")

while True:
time.sleep(1)

except Exception as e:
print(f"错误: {e}")
cleanup()
1
pwm_hw_test.py

第三步:使用示波器测量

  1. 打开示波器。
  2. 将探头连接到 物理引脚12 (GPIO 18) 和 GND。
  3. 在示波器上执行 自动设置 或手动调整:
    • 时基:调到 500μs/div 左右,以清晰看到1kHz的周期。
    • 电压刻度:调到 1V/div 或 2V/div。
    • 触发:设置为边沿触发,源选择你连接的通道,电平调到约1.6V (3.3V的一半)。
  4. 你应该能立即在屏幕上看到一个稳定的 方波。
  5. 进行测量:
    • 频率:使用示波器的测量功能,选择“频率”,读数应接近 1.000 kHz。
    • 占空比:选择“占空比”测量,读数应接近 50.0%。
    • 电压:高电平应在 3.3V 左右,低电平在 0V。

软件PWM

rpi-001-expriment

硬件PWM 10MHz

rpi-001-10M-expriment

树莓派PWM频率对比

软件PWM (RPi.GPIO)

  • 最高频率:约 1-10kHz (实际使用建议 ≤1kHz)
  • 限制因素:Python解释器性能、系统负载
  • 特点:所有GPIO都可用,但精度差、抖动明显

硬件PWM (pigpio/gpiozero)

  • 最高频率:理论上可达 125MHz (树莓派4)
  • 实际可用频率:通常 1kHz - 30MHz
  • 支持引脚:仅 GPIO12, GPIO13, GPIO18, GPIO19

具体频率对比表

方法 最高频率 推荐频率 精度 稳定性
RPi.GPIO软件PWM ~10kHz ≤1kHz 低 差
pigpio硬件PWM 125MHz 1kHz-30MHz 极高 优秀
gpiozero硬件PWM 125MHz 1kHz-30MHz 极高 优秀
Linux PWM子系统 125MHz 1kHz-30MHz 极高 优秀

第四步:实验扩展

修改代码中的 pwm.start(50),将 50 改为 10 (10%占空比) 或 90 (90%占空比),重新运行程序。再次用示波器测量,你会看到波形的“高电平”部分明显变短或变长,但频率保持不变。

恭喜你! 你已经成功地用软件生成一个精确的硬件信号,并用示波器完成了验证。这是嵌入式系统调试中最基础的技能之一。

compile the kernel

clone和compile在容器中(虚拟机)操作

1
2
3
4
docker run --privileged -d -it --name angel -v `pwd`:/root/code -w /root/code ghcr.io/cybrid-systems/dev
docker exec -it --detach-keys="ctrl-z,z" angel /bin/zsh
git config --global user.name $your_name
git config --global user.email $your_email

clone code

1
2
git clone https://github.com/torvalds/linux.git
cd linux

compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 生成默认配置
make defconfig

# 2. 启用调试选项
./scripts/config --enable DEBUG_KERNEL
./scripts/config --enable DEBUG_INFO
./scripts/config --enable KGDB
./scripts/config --enable FTRACE
./scripts/config --set-val DEBUG_INFO_DWARF4 y

# 3. 自动处理依赖关系
make olddefconfig

# 4. 可选:检查配置
grep -E "DEBUG_KERNEL|DEBUG_INFO|KGDB|FTRACE" .config

# 5. 编译内核
make CC=clang -j$(nproc)
bear -- make CC=clang LLVM=1 -j$(nproc)
make CC=clang binrpm-pkg LOCALVERSION=-debug -j$(nproc)
  • binrpm-pkg: Builds binary RPMs (kernel image, modules, headers). Use rpm-pkg for both binary and source RPMs.
  • LOCALVERSION=-debug: Appends a custom suffix (e.g., "-debug") to the kernel version for identification in GRUB.
1
2
3
dpkg -i kernel-*.deb
update-grub
reboot

在 Ubuntu 系统上,你可以通过 GRUB 引导菜单 在开机时手动选择要启动的内核(包括你刚编译安装的新内核,如带 -debug 后缀的那个)。这是最简单、最安全的临时/测试方式,不需要修改配置文件。

步骤:开机时选择内核

  1. 重启电脑
    执行 sudo reboot 或正常重启系统。

  2. 进入 GRUB 菜单

    • 在开机过程中(看到紫色/黑色屏幕或厂商 logo 后),立即按住 Shift 键(大多数 BIOS/UEFI 系统适用)。
    • 如果是纯 UEFI 系统且 Shift 不起作用,试试 按住 Esc 键 或 快速连按 Shift。
    • 目的是强制显示 GRUB 菜单(默认情况下,如果只有一个内核且超时为 0,它会直接启动而不显示菜单)。
  3. 在 GRUB 菜单中选择内核

    • 你会看到类似这样的主菜单(紫色背景):
      • Ubuntu
      • Advanced options for Ubuntu
      • (可能还有 Windows 或其他系统,如果是双系统)
    • 用 方向键 ↓ 选中 Advanced options for Ubuntu(Ubuntu 高级选项),按 Enter 进入子菜单。
  4. 在高级选项中挑选内核

    • 子菜单会列出所有已安装的内核版本,例如:
      • Ubuntu, with Linux 6.x.x-debug(你新编译的那个,通常带自定义后缀)
      • Ubuntu, with Linux 6.x.x-generic(旧的或默认的)
      • Ubuntu, with Linux 6.x.x-debug (recovery mode)
    • 用 方向键 选中你想要的内核(比如新编译的那个),然后按 Enter 启动。
  5. 启动成功后验证
    进入系统后,打开终端运行:

    1
    2
    3
    4
    5
    6
    7
       uname -r
    ```
    输出应该显示你选择的内核版本(例如 `6.x.x-debug`)。

    ### 额外小技巧
    - **临时只下一次启动某个内核**(不改默认):
    在进入系统后运行(替换成你想要的内核名称,从 `/boot/grub/grub.cfg` 中 grep 找):
    sudo grub-reboot "Advanced options for Ubuntu>Ubuntu, with Linux 6.x.x-debug" sudo reboot
    1
    2
    3
    4
      只影响下一次重启,下下次又恢复默认。

    - **如果 GRUB 菜单一直不显示**(超时为 0):
    编辑 `/etc/default/grub`:
    sudo nano /etc/default/grub
    1
    把 `GRUB_TIMEOUT=0` 改成 `GRUB_TIMEOUT=10`(单位:秒),保存后运行:  
    sudo update-grub
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
      以后开机就会显示菜单 10 秒。

    - **想永久默认新内核**(不再手动选):
    参考之前回复:修改 `/etc/default/grub` 中的 `GRUB_DEFAULT=` 为新内核的完整菜单路径(或编号 0,如果它排第一),然后 `sudo update-grub`。

    这样操作后,你就可以自由测试不同内核了。如果新内核启动失败(比如黑屏、panic),重启再选回旧内核即可,非常安全。需要截图或具体报错也可以继续问!

    ## 使用 BusyBox 创建最小 rootfs 的步骤如下:

    0. gcc 15 fix

    ```bash
    Option 1: Disable the tc Applet (Recommended if You Don't Need It)
    The tc command is for traffic control and may not be essential for your use case. Disabling it avoids the problem without patching code.

    Run make menuconfig (or make config for text-based) in your BusyBox source directory.
    Navigate to Networking Utilities > Disable tc.
    Save and exit.
    Re-run make to build BusyBox.

    Solution: Patch the Detection Script
    Edit scripts/kconfig/lxdialog/check-lxdialog.sh to update the test program for compatibility with modern GCC.

    Open the file:
    nano scripts/kconfig/lxdialog/check-lxdialog.sh (or use your preferred editor).
    Locate the section around lines 49-55 (may vary slightly) that echoes the test code:textecho "#include CURSES_LOC
    main() {}
    EOF
    Change it to:textecho "#include CURSES_LOC
    int main(void) { return 0; }
    EOF
    This adds an explicit int return type and a return statement to satisfy stricter standards.
    Using void for parameters avoids potential implicit declaration issues.

    Save and exit.
    Optionally, clean up:
    make clean
    Retry:
    make menuconfig

    This should now detect ncurses correctly and launch the menuconfig interface.

  6. 下载并编译 BusyBox

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
       wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
    tar xf busybox-1.36.1.tar.bz2
    cd busybox-1.36.1
    make defconfig
    make menuconfig # 可选:静态编译 Settings → Build static binary (no shared libs)
    # 2. 用 sed 两行命令完成上述两项修改
    sed -i 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
    sed -i 's|CONFIG_PREFIX=.*|CONFIG_PREFIX="./rootfs"|' .config
    # 3. 让 Kbuild 自动补齐依赖(重要!)
    make oldconfig >/dev/null </dev/null
    make -j$(nproc)
    make install

这里回到mac宿主机。

  1. 创建必要的目录结构
1
2
cd rootfs
mkdir -p {etc,dev,proc,sys,tmp,var,usr/bin,usr/sbin,lib,root}
  1. 创建基础设备节点
1
2
3
4
5
6
7
8
cd rootfs/dev

# 创建设备文件
sudo mknod console c 5 1
sudo mknod null c 1 3
sudo mknod zero c 1 5
sudo mknod ttyAMA0 c 204 64
sudo chmod 666 console null zero ttyAMA0
  1. 创建初始化脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cd rootfs
cat > init << 'EOF'
#!/bin/sh

echo "Starting system..."

# 挂载必要文件系统
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp

# 设置终端
exec </dev/ttyAMA0 >/dev/ttyAMA0 2>&1

# 设置环境变量
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
export HOME=/root
export USER=root

# 显示系统信息
echo "=== BusyBox Linux ==="
echo "Kernel: $(uname -r)"
echo "System is ready!"

# 启动shell(修复job control)
exec setsid sh -c 'exec sh </dev/ttyAMA0 >/dev/ttyAMA0 2>&1'
EOF
chmod +x init
  1. 创建简单的fstab
1
2
3
4
5
cat > etc/fstab << 'EOF'
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
EOF
  1. 创建rootfs镜像
1
2
3
4
5
6
7
8
9
10
11
12
cd rootfs

# 创建CPIO格式的rootfs(用于initrd)
find . | cpio -H newc -o | gzip > ../rootfs.cpio.gz

# 或者创建ext4文件系统镜像
dd if=/dev/zero of=../rootfs.ext4 bs=1M count=64
mkfs.ext4 ../rootfs.ext4
mkdir -p /mnt/rootfs
sudo mount ../rootfs.ext4 /mnt/rootfs
sudo cp -ra . /mnt/rootfs/
sudo umount /mnt/rootfs
  1. 使用QEMU启动
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a72 \
    -smp 4 \
    -m 2G \
    -kernel linux/arch/arm64/boot/Image \
    -initrd busybox-1.36.1/rootfs.cpio.gz \
    -append "console=ttyAMA0 rdinit=/init" \
    -nographic \
    -no-reboot
0%