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

这里注意不要用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
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

✅ 今天学会:

认识时间变量 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,表格绘制,感觉变化的向量。 要数形结合,结合前面变量、函数、向量的概念,让学生体会运动物体的轨迹,当时举的例子是抛一个🎾,水平的力让球有水平的动能,产生位移随时间变化,垂直方向是重力,高度随时间变化,产生轨迹。接下来学点概率呗。

四年级数学·暑期第二课

《小小向量大冒险——用 Mathematica 玩“位置魔法”》 (完整课件,1 课时 45 min)

————————— 【课程包】 PPT 10 页 + 学生讲义 1 张 + 教师 Mathematica 笔记本 1 份
(全部放在同名文件夹,打开即用)

————————— 课前 2 分钟 教师把教室地面贴成 6×6 方格,原点 (0,0) 贴星星贴纸,孩子踩格子热身。

————————— ## 课件流程

Slide 1 题目 & 目标
• 今天学会:用“小箭头”表示走路,用电脑把箭头画出来。
• 回家任务:画一张“我的上学路线图”。

Slide 2 故事
“小机器人 Turtle 收到两张藏宝图,先向右 3、向上 2,再向右 1、向上 3,它最后停在哪?”
(配图:两个箭头首尾相连)

Slide 3 动手踩格子
叫 2 位学生上台:
1 号同学从原点出发,按第一张图走;
2 号同学从 1 号终点出发,按第二张图走。
全班一起喊出最终格子坐标 (4,5)。
→ 把体验翻译成数学:
v = {3, 2},w = {1, 3},v + w = {4, 5}

Slide 4 Mathematica 第一次亮相
教师现场运行(大屏投影):

1
2
3
4
5
6
7
8
9
v = {3, 2}; w = {1, 3};
A = {0, 0};
Graphics[{
Red, Arrow[{A, A + v}],
Blue, Arrow[{A + v, A + v + w}],
Green, Arrow[{A, A + v + w}],
Black, PointSize[0.05], Point[A + v + w]
}, Axes -> True, GridLines -> Automatic,
PlotRange -> {{-1, 6}, {-1, 6}}]

孩子看到红箭头 + 蓝箭头 = 绿箭头,形状像滑梯。

Slide 5 向量加法公式(老师板书)
v = (x₁, y₁)
w = (x₂, y₂)
v + w = (x₁ + x₂, y₁ + y₂)
口诀:横加横,竖加竖。

Slide 6 “魔法缩放”滑条
教师拖动滑条 s,箭头忽大忽小:

1
2
3
4
5
6
Manipulate[
Graphics[{
Red, Arrow[{{0, 0}, s*{3, 2}}]
}, Axes -> True, PlotRange -> {{-5, 5}, {-5, 5}}],
{s, -2, 2, 0.1}
]

学生喊:
“s = 2 变两倍!”
“s = -1 反方向!”

Slide 7 长度(拓展给学有余力的孩子)
|v| = √(x² + y²) (老师口算 3²+2²=13,电脑 √13≈3.6)
告诉孩子:箭头越长,走路越远。

Slide 8 小挑战
教师发一张练习纸:
1. 画向量 u = {2,4}。
2. 计算并画出 2u。
3. 写出 2u 的长度。
(5 min 完成,同桌交换批改)

Slide 9 回家任务
• 量一量:从“家”到“学校”横走几格,竖走几格,写成向量 h。
• 拍照发群里,老师用 Mathematica 把全班的 h 画成一张“上学地图”。

Slide 10 预告下一课
“把 4 个箭头围成四边形——发现平行四边形面积的秘密!”

—————————
学生讲义(A5 双面)

正面:
【向量小卡片】
v = (→ , ↑)  w = (→ , ↑)  v + w = ( , )

【魔法缩放】
把 v 放大 2 倍:2v = ( , )

背面:
【回家任务单】
1. 我的上学向量 h = (___ , ___)。
2. 画在方格纸上,拍照。
3. 明天带来,拼成“全班上学地图”!

—————————
教师 Mathematica 笔记本(已写好,直接运行)
文件名:Lesson2_Vector.nb
包含:
• 踩格子演示 cell
• 加法动画 cell
• 缩放滑条 cell
• 自动收集学生照片并绘图的函数:

1
2
3
DrawClassMap[list_] := 
Graphics[{Arrow[{{0, 0}, #}] & /@ list}, Axes -> True,
PlotRange -> {{-10, 10}, {-10, 10}}]

—————————
一句话总结(给孩子)
“把两个走路指令加起来,就是一次走到底!”

课程记录

开场讲了下x,y坐标,然后家到学校的走法。接下来用讯飞本子上画了两个向量,以及他们相加,进入第一个mathematica的作图,结合代码进行讲解调试。

第二步讲了向量的加法,具体计算步骤,然后给出公式。

第三步讲了标量乘向量,用methematica的可操作图进行讲解分析。

进一步讲解了norm的计算,估计v{6, 4}的长度。介绍了底层的计算原理。

最后让果果画出u={3, 2}和2u。

x86的一堆软件重新装一下,主要是emacs重新编译native。

emacs 30.1重新编译

prepaer

1
2
3
4
5
brew install autoconf automake cmake libgccjit gnutls texinfo jansson tree-sitter gcc libgccgit
curl -O https://mirrors.ustc.edu.cn/gnu/emacs/emacs-30.1.tar.xz
tar -xvf emacs-30.1.tar.xz
cd emacs-30.1
./autogen.sh

. gcc_env.sh

1
2
3
export CC="/opt/homebrew/opt/gcc/bin/gcc-15"
export CXX="/opt/homebrew/opt/gcc/bin/g++-15"
export LIBRARY_PATH="/opt/homebrew/opt/gcc/lib/gcc/15:$LIBRARY_PATH"

configure and make

1
2
3
4
5
6
7
8
./configure \
--with-native-compilation \
--with-json \
--with-tree-sitter \
--with-gnutls \
--without-ns
make -j
make install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ doom doctor
The doctor will see you now...

> Checking your Emacs version...
> Checking for Doom's prerequisites...
> Checking for Emacs config conflicts...
> Checking for missing Emacs features...
> Checking for private config conflicts...
> Checking for common environmental issues...
> Checking for stale elc files...
> Checking for problematic git global settings...
> Checking Doom Emacs...
✓ Initialized Doom Emacs 3.0.0-pre
✓ Detected 37 modules
✓ Detected 133 packages
> Checking Doom core for irregularities...
Found Symbols Nerd Font Mono
> Checking for stale elc files in your DOOMDIR...
> Checking your enabled modules...

Everything seems fine, happy Emacs'ing!

好嘞!整得挺明白啊老铁!Emacs 整得溜光水滑的,这回可劲儿造吧!代码敲得咔咔的,插件跑得嗖嗖的,那叫一个得劲儿!有啥不明白的随时吱声,咱这嘎达给你支棱起来!开心就完事儿了! 😄

第一课:变量与函数——从欧姆定律开始

(适合三年级以上,结合电路实验)

📌 课程目标

  1. 理解“变量”:在电路中,电压(V)、电流(I)、电阻(R)都是可以变化的量,它们就是变量
  2. 理解“函数”:欧姆定律(V = I × R)就是一个函数,它告诉我们变量之间的关系
  3. 动手实验:用真实的电路测量、观察变量如何变化,并用万用表记录数据,绘制函数曲线。

🔧 实验材料

  • 电路元件:电池(9V 或 2节AA)、电阻(1kΩ、10kΩ)、电位器(可调电阻)、LED、面包板(可选)
  • 测量工具:万用表(测电压、电流、电阻)
  • 其他:导线、纸笔(记录数据)、坐标纸(画函数图)

🚀 实验步骤

1. 认识变量:电压、电流、电阻

🔹 电压(V):电池的“推力”,单位是伏特(V)。
🔹 电流(I):电子的“流动”,单位是安培(A),实验中常用毫安(mA)。
🔹 电阻(R):阻碍电流的“关卡”,单位是欧姆(Ω)。

小实验:用万用表测量

  • 测电池电压(V):万用表拨到 DCV 20V,红表笔接电池+,黑表笔接电池-。
  • 测电阻(R):万用表拨到 Ω(欧姆档),直接测电阻的阻值。
  • 测电流(I):万用表拨到 DCA 20mA串联进电路(后面实验会做)。

2. 欧姆定律实验:V = I × R

电路搭建:

1
电池 (+) —— 电位器(可调 R) —— 固定电阻(保护 LED) —— LED —— 电池 (-)
实验步骤:
1. 调节电位器(改变 R),用万用表测量当前的电阻值(R)。
2. 串联万用表(测电流 I)
- 把万用表拨到 DCA 20mA断开 LED 和电阻的连接,把表笔串联进去(红表笔接电阻,黑表笔接 LED)。
3. 记录数据
- 调节电位器 5 次,每次记录 R(Ω)I(mA)

实验次数 电阻 R (Ω) 电流 I (mA) 计算 V = I × R
1 1000 9.0 9.0 × 1000 = 9V
2 2000 4.5 4.5 × 2000 = 9V
3 3000 3.0 3.0 × 3000 = 9V
4 4000 2.25 2.25 × 4000 = 9V
5 5000 1.8 1.8 × 5000 = 9V

观察规律:
电阻 R 变大,电流 I 变小(反比关系)。
V = I × R 始终≈电池电压(9V),验证欧姆定律!


3. 绘制函数曲线:I = V / R

在坐标纸上:
- X 轴:电阻 R(Ω)
- Y 轴:电流 I(mA)
- 画点:把实验数据标在图上,连成曲线。

结论:
- 这条曲线就是 函数 I = V / R 的图形!
- 电阻 R 是输入变量,电流 I 是输出变量,欧姆定律就是它们之间的关系!


4. 计算机思维连接:变量 & 函数

🔹 变量就像“盒子”
- 在计算机里,RIV 都是变量,可以存储不同的值。
- 比如:

1
2
3
4
R = 1000  # 电阻是 1000Ω
V = 9 # 电池是 9V
I = V / R # 计算电流 I
print(I) # 输出 0.009 A(即 9mA)
🔹 函数就像“魔法机器”
- 欧姆定律就是一个函数:
1
2
def calculate_current(V, R):
return V / R
- 输入 VR,输出 I


🎯 课后挑战

  1. 改变电池电压(V):用 2 节 AA 电池(3V)再做实验,看看 IR 的关系是否还是 I = V / R
  2. 用 Scratch 模拟:在 Scratch 里做一个“欧姆定律计算器”,输入 VR,自动计算 I

📝 总结

变量:电路中可以变化的量(V、I、R)。
函数:变量之间的关系(欧姆定律 V = I × R)。
实验:用真实电路测量数据,画出函数曲线!
计算机连接:变量存储数据,函数计算关系!

下一课:电容充放电——时间变量和指数函数! 🚀

课程回顾

mathematica代码

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
data = {{3.0, 3.0, 1}, {3.0, 1.5, 2}, {3.0, 1.3, 2.2}, {3.7, 1.7, 
2.2}, {3.0, 0.8, 3.3}, {3.8, 1.1, 3.3}, {4.5, 1.3, 3.3}, {3.5,
0.7, 4.7}, {4.8, 1, 4.7}, {4.8, 0.9, 5.1}, {4.3, 0.8, 5.1}};

(*提取电压=3.0V的数据并按电阻排序*)
constantVoltageData = Select[data, #[[1]] == 3.0 &];
sortedData = SortBy[constantVoltageData, #[[3]] &];

ListLinePlot[sortedData[[All, {3, 2}]], PlotMarkers -> Automatic,
Frame -> True, FrameLabel -> {"电阻 (k\[CapitalOmega])", "电流 (mA)"},
PlotLabel -> "电压恒定(3.0V): 电阻\[UpArrow], 电流\[DownArrow]",
FrameStyle -> Directive[Black, 12], GridLines -> Automatic,
ImageSize -> 400]
(*按电阻值分组*)groupedData = GatherBy[data, #[[3]] &];
(*只保留有多个数据点的组*)
multiPointGroups = Select[groupedData, Length[#] > 1 &];

ListLinePlot[#[[All, {1, 2}]] & /@ multiPointGroups,
PlotMarkers -> Automatic, Frame -> True,
FrameLabel -> {"电压 (V)", "电流 (mA)"},
PlotLabel -> "电阻恒定: 电压\[UpArrow], 电流\[UpArrow]",
FrameStyle -> Directive[Black, 12],
PlotLegends -> ("R = " <> ToString[#[[1, 3]]] <>
" k\[CapitalOmega]" & /@ multiPointGroups),
GridLines -> Automatic, ImageSize -> 400]
constantVoltageData = {{3.0, 3.0, 1}, {3.0, 1.5, 2}, {3.0, 1.3,
2.2}, {3.0, 0.8, 3.3}};
TableForm[constantVoltageData,
TableHeadings -> {None, {"电压(V)", "电流(mA)", "电阻(k\[CapitalOmega])"}}]
(*绘制实验数据点*)
dataPoints =
ListPlot[constantVoltageData[[All, {3, 2}]],
PlotStyle -> {Red, PointSize[0.02]}, Frame -> True,
FrameLabel -> {"电阻R (k\[CapitalOmega])", "电流I (mA)"},
GridLines -> Automatic, PlotRange -> All];

(*绘制理论函数 I=3.0/R*)
theoreticalPlot =
Plot[3.0/r, {r, 0.5, 5}, PlotStyle -> Blue,
PlotLegends -> {"I = 3.0/R"}];

(*合并图形*)
Show[theoreticalPlot, dataPoints,
PlotLabel -> "函数关系: 电流I = 电压V / 电阻R"]
V = 3.0;
predictedI = V/5 (*输出:0.6 mA*)
Manipulate[
Plot[V/r, {r, 1, 10}, Frame -> True,
FrameLabel -> {"R (k\[CapitalOmega])", "I (mA)"}], {V, 1, 10, 0.1},
Initialization :> (V = 3.0)]
TableForm[Table[{r, 3.0/r}, {r, 1, 5, 0.5}],
TableHeadings -> {None, {"R (k\[CapitalOmega])", "I (mA)"}}]
  1. 实验测试了多组数据记录
  2. 用mathematica进行可视化讲解
  3. 用计算器进行一些验证
  4. 讨论分析为什么理论和测量的偏差
  5. 最后给出V=3的I和R的函数,计算和实验
0%