课程目标(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"
  • 大小可以改:3080100
  • 样式可以变:"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. 单词拼错circlcircle

差异化教学

  • 进度快的:尝试starellipse等更多图形
  • 进度慢的:专注于掌握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:ubuntu24.04

这里注意不要用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/div2V/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
3
git clone https://mirrors.tuna.tsinghua.edu.cn/git/linux-stable.git
cd linux-stable
gco v5.15.194

compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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)

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

  1. 下载并编译 BusyBox
    1
    2
    3
    4
    5
    6
    7
    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)
    make -j$(nproc)
    make install
1
2
3
4
5
6
7
8
9
# 1. 先拿一份默认配置
make defconfig

# 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

这里回到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-stable/arch/arm64/boot/Image \
    -initrd busybox-1.36.1/rootfs.cpio.gz \
    -append "console=ttyAMA0 rdinit=/init" \
    -nographic \
    -no-reboot

✅ 今天学会:

认识时间变量 t

掌握位置函数的魔法公式

画出运动向量的轨迹 🎮 终极任务:设计你的精灵运动动画!

复习向量 - 从静态到动态

"上节课的'走路箭头'还记得吗?"

  • 第二课回顾:静态向量 {3,2} = 从原点走到(3,2)
  • 新发现:如果这个箭头会动呢?

课堂提问:箭头变长时,终点位置如何变化?(引出位置向量动态化)

认识新变量 - 时间魔法师

"小精灵的隐形遥控器 - 时间 t"

实验:学生用秒表计时,教师移动精灵玩偶

t(秒) 0 1 2
位置 起点 ? ?

t 是变量 - 像第一课的电阻/电压,可以自由改变!

"把时间翻译成位置的魔法"

教室活动:

学生A匀速向右走:x(t) = 2×t(步数)

学生B模拟下落:y(t) = 5 - 5×t²

板书公式:

1
2
3
位置函数: 
x(t) = 起点x + 速度x × t
y(t) = 起点y + 速度y × t - 5×t²

函数机器图解
graph LR
    t["输入 t"] --> 机器["魔法公式"] --> 输出["输出 (x(t),y(t))"]

联系第一课: 欧姆定律:输入V,R → 输出I 位置函数:输入t → 输出位置向量!

动手计算 - 我的精灵设计

"设计你的精灵飞行"

时间 t 计算 x(t)=1+2×t i计算 y(t)=3+4×t-5×t² 位置向量
0 1 3 (1, 3)
1 3 3+4-5=2 (3, 2)
2 5 3+8-20=-9 (5, -9)

任务:

  1. 选精灵(飞机/小鸟/超人)
  2. 自定起点和速度
  3. 完成表格计算

"把位置向量连起来!"

  • 学生活动:在坐标纸画出三个位置向量终点
  • 重大发现:三点连成抛物线!
1
ListPlot[{{1,3},{3,2},{5,-9}}, PlotRange->{{0,6},{-10,5}}]

教师强调: 每个位置向量是瞬间快照 连起来就是运动轨迹(函数的图像)

教学总结

通过时间变量、x(t)、y(t),两个位置函数组成向量。通过不同时间的t,表格绘制,感觉变化的向量。 要数形结合,结合前面变量、函数、向量的概念,让学生体会运动物体的轨迹,当时举的例子是抛一个🎾,水平的力让球有水平的动能,产生位移随时间变化,垂直方向是重力,高度随时间变化,产生轨迹。接下来学点概率呗。

0%