Rockchip RK3399 - Platform驱动(DMA&i2s0)
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
Platfrom driver提供了配置/使能SoC音频接口的能力;Plaftrom驱动分为三个部分:dma driver、cpu dai driver、dsp driver。
(1) cpu dai driver:在嵌入式系统里面通常指SoC的 I2S、PCM 总线控制器,负责把音频数据从I2S tx FIFO 搬运Codec(这是回放的情形,录制则方向相反)。每个cpu dai driver必须提供以下功能:
- DAI描述信息;
- DAI配置信息;
- PCM描述信息;
- 系统时钟配置;
- 挂起和恢复(可选);
更多信息参考: Documentation/sound/soc/dai.rst。
(2) DMA driver :负责把dma buffer中的音频数据搬运到 I2S tx FIFO。值得留意的是:某些情形下是不需要dma操作的,比如Modem和Codec直连,因为Modem本身已经把数据送到 FIFO 了,这时只需启动codec_dai 接收数据即可;DMA driver可以参考soc/pxa/pxa2xx-pcm.c;
(3) DSP driver
每个DSP driver通常提供以下功能:
- DAPM拓扑;
- Mixer controls;
- DMA IO to/from DSP buffers (if applicable);
- Definition of DSP front end (FE) PCM devices;
更多信息参考: Documentation/sound/soc/platform.rst,https://www.kernel.org/doc/html/v6.3/sound/soc/platform.html。驱动代码位于sound/soc/rockchip/rockchip_i2s.c文件。
一、设备树配置
1.1 设备节点i2s0
设备节点i2s0定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi文件:
i2s0: i2s@ff880000 { compatible = "rockchip,rk3399-i2s", "rockchip,rk3066-i2s"; reg = <0x0 0xff880000 0x0 0x1000>; rockchip,grf = <&grf>; interrupts = <GIC_SPI 39 IRQ_TYPE_LEVEL_HIGH 0>; dmas = <&dmac_bus 0>, <&dmac_bus 1>; dma-names = "tx", "rx"; clock-names = "i2s_clk", "i2s_hclk"; clocks = <&cru SCLK_I2S0_8CH>, <&cru HCLK_I2S0_8CH>; pinctrl-names = "bclk_on", "bclk_off"; pinctrl-0 = <&i2s0_8ch_bus>; pinctrl-1 = <&i2s0_8ch_bus_bclk_off>; power-domains = <&power RK3399_PD_SDIOAUDIO>; #sound-dai-cells = <0>; status = "disabled"; };
这是Rockchip RK3399中I2S0设备节点描述。它包括以下属性:
- compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
- reg:指定I2S0控制器的基地址和地址空间大小,从0xff880000到0xff881000共有0x1000个字节的寄存器空间,其中0xff880000为I2S0寄存器基地址;
- rockchip,grf:设置为grf设备节点,GRF表示的是通用寄存器文件,由许多用于系统控制的寄存器组成,GRF可以分为两部分;
- GRF:used for generalnon-secure system;
- PMUGRF:used for always on sysyem;
- interrupts:指定I2S控制器的中断号为GIC_SPI 39,并且取值方式为IRQ_TYPE_LEVEL_HIGH,意味着中断信号为高电平触发;
- dmas:指定数据传输时使用的DMA控制器,第一个表示tx数据使用的DMA控制器,第二个表示rx数据使用的DMA控制器;
- dma-names:分别对应"tx"和"rx"的DMA名称;
- clock-names:指定时钟名称,"i2s_clk"表示I2S0控制器时钟,"i2s_hclk" 表示I2S总线时钟;
- clocks:i2s_clk时钟来自SCLK_I2S0_8CH,i2s_hclk时钟来自 HCLK_I2S0_8CH;
- pinctrl-names:指定设备pinctrl配置集合;
- pinctrl-0:设置bclk_on状态对应的引脚配置为i2s0_8ch_bus,这里主要配置I2S0相关引脚复用为I2S功能;
- pinctrl-1:设置bclk_off状态对应的引脚配置为i2s0_8ch_bus_bclk_off,这里主要配置I2S0相关引脚复用为I2S功能,但是SCLK被设置为了GPIO,因此此时I2S0功能是禁用的;
- power-domains:指定设备隶属于的电源域,这里是 RK3399_PD_SDIOAUDIO;
- #sound-dai-cells:表示定义这个节点的sound DAI数据单元格的数量,这里为0表示没有单元格;
- status:表示设备状态,这里 "disabled" 表示该设备当前是禁用状态;
其中设备节点grf定义如下:
grf: syscon@ff770000 { compatible = "rockchip,rk3399-grf", "syscon", "simple-mfd"; reg = <0x0 0xff770000 0x0 0x10000>; // GRF基地址0xff770000 大小64kb #address-cells = <1>; #size-cells = <1>; io_domains: io-domains { compatible = "rockchip,rk3399-io-voltage-domain"; status = "disabled"; }; ...... }
我们需要在arch/arm64/boot/dts/rockchip/rk3399-evb.dts文件添加如下属性,启用I2S0控制器:
&i2s0 { rockchip,playback-channels = <8>; rockchip,capture-channels = <8>; status = "okay"; };
其中:
- rockchip,playback-channels:为最大播放通道数;
- rockchip,capture-channels:为最大录音通道数;
这里配置为8,以播放音频为例我谈一谈我个人的理解,应该是RK3399 I2S0有4条数据线可以用来传输收音频数据,I2S0_SDI0、I2S0_SDI1、I2S0_SDI02、I2S0_SDI03;由于在每条数据线上传输左通道、右通道的信号,因此4*2=8,一共就是8个通道;
关于设备节点属性可以参考文档Documentation/devicetree/bindings/sound/rockchip-i2s.yaml。
而RK3399 I2S控制器驱动代码位于sound/soc/rockchip/rockchip_i2s.c文件。
1.2 引脚配置节点i2s0_8ch_bus
引脚配置节点i2s0_8ch_bus和i2s0_8ch_bus_bclk_off定义在pinctrl设备节点下:
i2s0 { i2s0_2ch_bus: i2s0-2ch-bus { rockchip,pins = <3 RK_PD0 1 &pcfg_pull_none>, <3 RK_PD1 1 &pcfg_pull_none>, <3 RK_PD2 1 &pcfg_pull_none>, <3 RK_PD3 1 &pcfg_pull_none>, <3 RK_PD7 1 &pcfg_pull_none>, <4 RK_PA0 1 &pcfg_pull_none>; }; i2s0_8ch_bus: i2s0-8ch-bus { rockchip,pins = <3 RK_PD0 1 &pcfg_pull_none>, <3 RK_PD1 1 &pcfg_pull_none>, <3 RK_PD2 1 &pcfg_pull_none>, <3 RK_PD3 1 &pcfg_pull_none>, <3 RK_PD4 1 &pcfg_pull_none>, <3 RK_PD5 1 &pcfg_pull_none>, <3 RK_PD6 1 &pcfg_pull_none>, <3 RK_PD7 1 &pcfg_pull_none>, <4 RK_PA0 1 &pcfg_pull_none>; }; i2s0_8ch_bus_bclk_off: i2s0-8ch-bus-bclk-off { rockchip,pins = <3 RK_PD0 RK_FUNC_GPIO &pcfg_pull_none>, <3 RK_PD1 1 &pcfg_pull_none>, <3 RK_PD2 1 &pcfg_pull_none>, <3 RK_PD3 1 &pcfg_pull_none>, <3 RK_PD4 1 &pcfg_pull_none>, <3 RK_PD5 1 &pcfg_pull_none>, <3 RK_PD6 1 &pcfg_pull_none>, <3 RK_PD7 1 &pcfg_pull_none>, <4 RK_PA0 1 &pcfg_pull_none>; }; };
这里我们只关注i2s0_8ch_bus引脚配置节点,这里定义了9个引脚,管脚与具体的功能和电气特性如下:
- GPIO3_PD0:功能复用为I2S0_SCLK,电气特性配置为pcfg_pull_none;
- GPIO3_PD1:功能复用为I2S0_LRCK_RX,电气特性配置为pcfg_pull_none;
- GPIO3_PD2:功能复用为I2S0_LRCK_TX,电气特性配置为pcfg_pull_none;
- GPIO3_PD3:功能复用为I2S0_SDI0,电气特性配置为pcfg_pull_none;
- GPIO3_PD4:功能复用为I2S0_SDI1/I2S0_SDO3,电气特性配置为pcfg_pull_none;
- GPIO3_PD5:功能复用为I2S0_SDI2/I2S0_SDO2,电气特性配置为pcfg_pull_none;
- GPIO3_PD6:功能复用为I2S0_SDI3/I2S0_SDO1,电气特性配置为pcfg_pull_none;
- GPIO3_PD7:功能复用为I2S0_SDO0,电气特性配置为pcfg_pull_none;
- GPIO4_PA0:功能复用为I2S0_MCLK,电气特性配置为pcfg_pull_none;
1.3 时钟频率
这里我们看看一下时钟频率配置:
clock-names = "i2s_clk", "i2s_hclk"; clocks = <&cru SCLK_I2S0_8CH>, <&cru HCLK_I2S0_8CH>;
SCLK_I2S0_8CH、HCLK_I2S0_8CH为平台为时钟分配的特定的id,定义在drivers/clk/rockchip/clk-rk3399.c:
GATE(SCLK_I2S0_8CH, "clk_i2s0", "clk_i2s0_mux", CLK_SET_RATE_PARENT, RK3399_CLKGATE_CON(8), 5, GFLAGS), GATE(HCLK_I2S0_8CH, "hclk_i2s0", "hclk_perilp1", 0, RK3399_CLKGATE_CON(34), 0, GFLAGS),
他们都是gate类型的时钟,其中GATE宏定义在drivers/clk/rockchip/clk.h:
#define GATE(_id, cname, pname, f, o, b, gf) \ { \ .id = _id, \ .branch_type = branch_gate, \ .name = cname, \ .parent_names = (const char *[]){ pname }, \ .num_parents = 1, \ .flags = f, \ .gate_offset = o, \ .gate_shift = b, \ .gate_flags = gf, \ }
1.3.1 hclk_i2s0
我们以GATE(HCLK_I2S0_8CH, "hclk_i2s0", "hclk_perilp1", 0, RK3399_CLKGATE_CON(34), 0, GFLAGS)为例:
- id表示平台为时钟特定分配的id,这里被设置为了HCLK_I2S0_8CH;定义在include/dt-bindings/clock/rk3399-cru.h,值为468;
- name表示时钟的名称,这里设置为hclk_i2s0;
- parent_name为父时钟的名称,这里设置为hclk_perilp1;
- gate_offset表示控制时钟开关的寄存器偏移地址,这里设置为RK3399_CLKGATE_CON(34);
- gate_shift表示控制时钟开关bit位,这里设置为0;
宏RK3399_CLKGATE_CON定义在drivers/clk/rockchip/clk.h:
#define RK3399_CLKGATE_CON(x) ((x) * 0x4 + 0x300)
通过RK3399_CLKGATE_CON(34)可以得到寄存器偏移地址34*0x04+0x300=0x388,偏移0x388是CRU_CLKGATE_CON34寄存器。
在RK3399 datasheet中,我们可以找到名字为hclk_i2s0的时钟的信息,从下图可以看到其父时钟为ID为162(datasheet里这个ID和程序里id并不是相同的),可以在datasheet表中找到162代表的是hclk_perilp1
接着我们看一下CRU_CLKGATE_CON34寄存器,CRU_CLKGATE_CON34为Internal clock gating register34,其中位[0]含义如下:
可以看到位0为hclk_i2s0时钟使能位,低电平使能,高电平禁用。 那hclk_i2s0到底是什么时钟呢?官方给出的解释是I2S BUS时钟;
系统在播放音频文件时, 我们可以通过以下命令查看时钟频率,可以看到输出的频率为100MHz;
root@rk3399:/# cd / root@rk3399:/# aplay AbuduOffice.wav Playing WAVE 'AbuduOffice.mp3' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo root@rk3399:/# cat /sys/kernel/debug/clk/hclk_i2s0/clk_rate 100000000
1.3.2 clk_i2s0
我们以GATE(SCLK_I2S0_8CH, "clk_i2s0", "clk_i2s0_mux", CLK_SET_RATE_PARENT, RK3399_CLKGATE_CON(8), 5, GFLAGS)为例:
- id表示平台为时钟特定分配的id,这里被设置为了SCLK_I2S0_8CH;定义在include/dt-bindings/clock/rk3399-cru.h,值为86;
- name表示时钟的名称,这里设置为clk_i2s0;
- parent_name为父时钟的名称,这里设置为clk_i2s0_mux;
- gate_offset表示控制时钟开关的寄存器偏移地址,这里设置为RK3399_CLKGATE_CON(8);
- gate_shift表示控制时钟开关bit位,这里设置为5;
宏RK3399_CLKGATE_CON定义在drivers/clk/rockchip/clk.h:
#define RK3399_CLKGATE_CON(x) ((x) * 0x4 + 0x300)
通过RK3399_CLKGATE_CON(8)可以得到寄存器偏移地址8*0x04+0x300=0x320,偏移0x320是CRU_CLKGATE_CON8寄存器。
在RK3399 datasheet中,我们可以找到名字为clk_i2s0的时钟的信息,从下图可以看到其父时钟为ID为52,可以在datasheet表中找到52代表的是clk_i2s0_mux;
接着我们看一下CRU_CLKGATE_CON8寄存器,CRU_CLKGATE_CON8为Internal clock gating register8,其中位[5]含义如下:
可以看到位5为clk_i2s0时钟使能位,低电平使能,高电平禁用。 那clk_i2s0到底是什么时钟呢?官方给出的解释是I2S Controller时钟,因此我猜测应该就是I2S0模块输出的MCLK时钟;
系统在播放音频文件时, 我们可以通过以下命令查看时钟频率,可以看到输出的频率为11.2896MHz;
root@rk3399:/# cd / root@rk3399:/# aplay AbuduOffice.wav Playing WAVE 'AbuduOffice.mp3' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo root@rk3399:/# cat /sys/kernel/debug/clk/clk_i2s0/clk_rate 11289600
假设采样频率为44.1kHz,则MCLK=256*44100=11289600,可以看到这个时钟频率和通过命令查看到的是一致的,这也证明了我们的采样。
1.3.3 时钟链路
我们以时钟clk_i2s0为例,我们查找其父时钟:
root@rk3399:/# cat /sys/kernel/debug/clk/clk_i2s0/clk_parent
clk_i2s0_mux
接着我们查找clk_i2s0_mux的父时钟,因此类推,最终可以得到clk_i2s0所在的时钟链路;
- xin24m --> pll_cpll --> cpll --> clk_i2s0_div (分频器,CRU_CLKSEL_CON28寄存器位[6:0]用于设置分频系数)--> clk_i2s0_frac(gate,CRU_CLKGATE_CON8寄存器位[4]用于时钟使能) --> clk_i2s0_mux(多路选择,CRU_CLKSEL_CON28寄存器位[9:8]用于选择时钟源) --> clk_i2s0。
在Rockchip RK3399 - Codec驱动( Realtek ALC5651) 驱动中我们说过I2S0/IS1接口共用一个MCLK引脚,因此要想将clk_i2s0时钟输出到MCLK引脚还需要进行多路选择、以及与门相关的配置。
同理我们可以得到hclk_i2s0的时钟链路:xin24m --> pll_cpll --> cpll --> cpll_hclk_perilp1_src --> hclk_perilp1 --> hclk_i2s0。
二、Platform驱动
2.1 模块入口函数
我们定位到sound/soc/rockchip/rockchip_i2s.c文件的最后;
static struct platform_driver rockchip_i2s_driver = { .probe = rockchip_i2s_probe, .remove = rockchip_i2s_remove, .driver = { .name = DRV_NAME, // rockchip-i2s .of_match_table = of_match_ptr(rockchip_i2s_match), // 用于设备树匹配 .pm = &rockchip_i2s_pm_ops, }, }; module_platform_driver(rockchip_i2s_driver);
module_platform_driver这个宏之前已经介绍过多次了,其展开后等价于:
static int __init rockchip_i2s_driver_init(void) { return platform_driver_register(&(rockchip_i2s_driver)); } module_init(rockchip_i2s_driver_init); static void __exit rockchip_i2s_driver_exit(void) { platform_driver_unregister(&(rockchip_i2s_driver)); } module_exit(rockchip_i2s_driver_exit);
看到这里是不是有点意外,这里是通过platform_driver_register函数注册了一个名称为"rockchip-i2s"的platform驱动。
2.1.1 设备树匹配
在plaftrom总线设备驱动模型中,我们知道当内核中有platform设备和platform驱动匹配,会调用到platform_driver里的成员.probe,在这里就是rockchip_i2s_probe函数。
而platform设备和驱动匹配规则包括:
- of_driver_match_device(dev, drv);这一种是进行设备树的匹配;
- strcmp(pdev->name, drv->name);这一种就是将platform设备的名称和platform驱动的名称进行匹配;
of_driver_match_device定义在include/linux/of_device.h:
/** * of_driver_match_device - Tell if a driver's of_match_table matches a device. * @drv: the device_driver structure to test * @dev: the device structure to match against */ static inline int of_driver_match_device(struct device *dev, const struct device_driver *drv) { return of_match_device(drv->of_match_table, dev) != NULL; }
可以看到这里是调用of_match_device函数,遍历device_driver的of_match_table数组,然后和device的of_node进行匹配,该函数定义在drivers/of/device.c:
/** * of_match_device - Tell if a struct device matches an of_device_id list * @matches: array of of device match structures to search in * @dev: the of device structure to match against * * Used by a driver to check whether an platform_device present in the * system is in its list of supported devices. */ const struct of_device_id *of_match_device(const struct of_device_id *matches, const struct device *dev) { if ((!matches) || (!dev->of_node)) return NULL; return of_match_node(matches, dev->of_node); }
2.1.2 rockchip_i2s_match
device_driver的of_match_table数组rockchip_i2s_match定义如下:
static const struct of_device_id rockchip_i2s_match[] __maybe_unused = { { .compatible = "rockchip,px30-i2s", }, { .compatible = "rockchip,rk1808-i2s", }, { .compatible = "rockchip,rk3036-i2s", }, { .compatible = "rockchip,rk3066-i2s", }, { .compatible = "rockchip,rk3128-i2s", }, { .compatible = "rockchip,rk3188-i2s", }, { .compatible = "rockchip,rk3228-i2s", }, { .compatible = "rockchip,rk3288-i2s", }, { .compatible = "rockchip,rk3308-i2s", }, { .compatible = "rockchip,rk3328-i2s", }, { .compatible = "rockchip,rk3366-i2s", }, { .compatible = "rockchip,rk3368-i2s", }, { .compatible = "rockchip,rk3399-i2s", .data = &rk3399_i2s_pins }, { .compatible = "rockchip,rv1126-i2s", }, {}, };
因此当i2s0设备节点中的compatible与rockchip_i2s_match中某一个元素匹配,of_match_table函数将会返回true,即匹配成功。
i2s0: i2s@ff880000 { compatible = "rockchip,rk3399-i2s", "rockchip,rk3066-i2s"; ....... }
2.2 rockchip_i2s_probe
当platform设备和platform驱动匹配,会调用到platform_driver里的成员.probe,在这里就是rockchip_i2s_probe函数;
static int rockchip_i2s_probe(struct platform_device *pdev) { struct device_node *node = pdev->dev.of_node; const struct of_device_id *of_id; struct rk_i2s_dev *i2s; struct snd_soc_dai_driver *dai; struct resource *res; void __iomem *regs; int ret; i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL); // 1. 动态申请内存,数据结构类型为struct rk_i2s_dev if (!i2s) return -ENOMEM; spin_lock_init(&i2s->lock); // 初始化自旋锁 i2s->dev = &pdev->dev; // 初始化dev i2s->grf = syscon_regmap_lookup_by_phandle(node, "rockchip,grf"); // 2. 等价将of_parse_phandle和syscon_node_to_regmap,实现通过regmap来访问grf寄存器 if (!IS_ERR(i2s->grf)) { of_id = of_match_device(rockchip_i2s_match, &pdev->dev); if (!of_id || !of_id->data) return -EINVAL; i2s->pins = of_id->data; } /* try to prepare related clocks */ i2s->hclk = devm_clk_get(&pdev->dev, "i2s_hclk"); // 3. 根据时钟名称"i2s_hclk"获取时钟,设备节点属性clock-names、clocks、
// 指定了名字为"i2s_hclk"对应的时钟为<&cru HCLK_I2S0_8CH> if (IS_ERR(i2s->hclk)) { dev_err(&pdev->dev, "Can't retrieve i2s bus clock\n"); return PTR_ERR(i2s->hclk); } ret = clk_prepare_enable(i2s->hclk); // 时钟使能 if (ret) { dev_err(i2s->dev, "hclock enable failed %d\n", ret); return ret; } i2s->mclk = devm_clk_get(&pdev->dev, "i2s_clk"); // 4.根据时钟名称"i2s_clk"获取时钟,设备节点属性clock-names、clocks、
// 指定了名字为"i2s_clk"对应的时钟为<&cru SCLK_I2S0_8CH> if (IS_ERR(i2s->mclk)) { dev_err(&pdev->dev, "Can't retrieve i2s master clock\n"); ret = PTR_ERR(i2s->mclk); goto err_clk; } regs = devm_platform_get_and_ioremap_resource(pdev, 0, &res); // 5.获取I/O内存资源,并将其映射到内核虚拟地址空间中 if (IS_ERR(regs)) { ret = PTR_ERR(regs); goto err_clk; } i2s->regmap = devm_regmap_init_mmio(&pdev->dev, regs, // 6.将I2S0寄存器转化为regmap形式,这样regmap机制的regmap_write、regmap_read等API函数操作寄存器 &rockchip_i2s_regmap_config); if (IS_ERR(i2s->regmap)) { dev_err(&pdev->dev, "Failed to initialise managed register map\n"); ret = PTR_ERR(i2s->regmap); goto err_clk; } i2s->bclk_ratio = 64; i2s->pinctrl = devm_pinctrl_get(&pdev->dev); // 7.获取与设备相关联的pinctrl句柄 if (!IS_ERR(i2s->pinctrl)) { i2s->bclk_on = pinctrl_lookup_state(i2s->pinctrl, "bclk_on"); if (!IS_ERR_OR_NULL(i2s->bclk_on)) { i2s->bclk_off = pinctrl_lookup_state(i2s->pinctrl, "bclk_off"); if (IS_ERR_OR_NULL(i2s->bclk_off)) { dev_err(&pdev->dev, "failed to find i2s bclk_off\n"); ret = -EINVAL; goto err_clk; } } } else { dev_dbg(&pdev->dev, "failed to find i2s pinctrl\n"); } i2s_pinctrl_select_bclk_off(i2s); // 配置设备引脚的状态为bclk_off dev_set_drvdata(&pdev->dev, i2s); // 设置驱动数据 pm_runtime_enable(&pdev->dev); // 电源相关,使能设备的runtime pm功能 暂且忽略 if (!pm_runtime_enabled(&pdev->dev)) { ret = i2s_runtime_resume(&pdev->dev); if (ret) goto err_pm_disable; } ret = rockchip_i2s_init_dai(i2s, res, &dai); // 8. 初始化cpu dai driver if (ret) goto err_pm_disable; ret = devm_snd_soc_register_component(&pdev->dev, // 9. 注册component &rockchip_i2s_component, dai, 1); if (ret) { dev_err(&pdev->dev, "Could not register DAI\n"); goto err_suspend; } ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0); // 10 申请dma通道 if (ret) { dev_err(&pdev->dev, "Could not register PCM\n"); goto err_suspend; } return 0; err_suspend: if (!pm_runtime_status_suspended(&pdev->dev)) i2s_runtime_suspend(&pdev->dev); err_pm_disable: pm_runtime_disable(&pdev->dev); err_clk: clk_disable_unprepare(i2s->hclk); return ret; }
(1) 动态申请内存,数据结构类型为struct rk_i2s_dev,并调用dev_set_drvdata将其设置为驱动数据;
(2) 调用syscon_regmap_lookup_by_phandle函数,将GRF寄存器转化为regmap形式,这样regmap机制的regmap_write、regmap_read等API函数操作寄存器;GRF寄存器基地址为0xff770000,大小为64kb;;同时初始化成员pins为rk3399_i2s_pins:
static const struct rk_i2s_pins rk3399_i2s_pins = { .reg_offset = 0xe220, .shift = 11, };
偏移0xe220为GRF_SOC_CON8寄存器,寄存器信息如下,从下表可以看到寄存器位偏移11为i2s0_sdio_oe_n,表示位控制;
Bit | Attr | Reset Value | Description |
---|---|---|---|
31:16 | RW | 0x0000 |
write_enable bit0~15 write enable When bit 16=1, bit 0 can be written by software . When bit 16=0, bit 0 cannot be written by software; When bit 17=1, bit 1 can be written by software . When bit 17=0, bit 1 cannot be written by software; ...... When bit 31=1, bit 15 can be written by software . When bit 31=0, bit 15 cannot be written by software; |
15:14 | RO | 0x0 | reserved |
13:11 | RW | 0x0 |
2s0_sdio_oe_n i2s0 sdio_oe_n bit control |
10:7 | RW | 0x0 |
pcie_test_i pci test input |
6:1 | RW | 0x0 0 |
pcie_test_addr pci test address control |
0 | RW | 0x0 |
pcie_test_write pcie test write control 1'b0: disable
|
(3) 调用devm_clk_get根据时钟名称"i2s_hclk"获取I2S总线时钟,设备节点属性clock-names、clocks、指定了名字为"i2s_hclk"对应的时钟为<&cru HCLK_I2S0_8CH>;并调用clk_prepare_enable使能hclk;
(4) 调用devm_clk_get根据时钟名称"i2s_clk"获取I2C控制器时钟,设备节点属性clock-names、clocks、指定了名字为"i2s_clk"对应的时钟为<&cru SCLK_I2S0_8CH>;如果pm_runtime_enabled返回false,则clk使能是通过i2s_runtime_resume完成;
static int i2s_runtime_resume(struct device *dev) { struct rk_i2s_dev *i2s = dev_get_drvdata(dev); int ret; ret = clk_prepare_enable(i2s->mclk); if (ret) { dev_err(i2s->dev, "clock enable failed %d\n", ret); return ret; } regcache_cache_only(i2s->regmap, false); regcache_mark_dirty(i2s->regmap); ret = regcache_sync(i2s->regmap); if (ret) clk_disable_unprepare(i2s->mclk); return ret; }
(5) 调用devm_platform_get_and_ioremap_resource获取I/O内存资源,并将其映射到内核虚拟地址空间中;0xff880000为I2S0寄存器基地址;
reg = <0x0 0xff880000 0x0 0x1000>;
(6) 调用devm_regmap_init_mmio将I2S0寄存器转化为regmap形式,这样regmap机制的regmap_write、regmap_read等API函数操作寄存器;
(7) 调用devm_pinctrl_get获取与设备相关联的pinctrl句柄,并配置设备引脚的状态为bclk_off;
(8) 调用rockchip_i2s_init_dai初始化cpu dai driver;
(9) 调用devm_snd_soc_register_component注册的component,该函数会动态申请一个component,并将其添加到全局链表component_list中,同时会建立dai_driver与component的关系;
注册component完成后,snd_soc_dai,snd_soc_dai_driver、snd_soc_component、snd_soc_component_driver之间的关系如下图:
其中:
- 新建的snd_soc_component的名称为pdev->dev设备的名称,即ff880000.i2s;
- snd_soc_component的dai_list链表包含1个dai,dai的名称为ff880000.i2s;
- 每dai对应的dai driver的名称为NULL,未设置;
(10) 调用devm_snd_dmaengine_pcm_register申请dma通道。
2.3 总结
我们在分析了platform驱动的源码之后,我们大致可以得到一个结论,palform驱动注册流程主要包含一下几个步骤:
(1) 构造一个struct snd_soc_component_driver实例,比如这里的rockchip_i2s_component,用于描述palform驱动;需要初始化成员name;
(2) 构造一个struct snd_soc_dai_driver,比如这里的rockchip_i2s_dai,用于描述dai和 pcm的能力和操作;需要初始化成员name、probe、playback、capture、ops等;
(3) 调用devm_snd_soc_register_component注册component;
下面我们将会具体来解析rockchip_i2s_probe函数源码,如果不感兴趣可以忽略后面章节介绍的内容。
三、相关数据结构
3.1 struct rk_i2s_dev
struct rk_i2s_dev定义在sound/soc/rockchip/rockchip_i2s.c:
struct rk_i2s_dev { struct device *dev; struct clk *hclk; struct clk *mclk; struct snd_dmaengine_dai_dma_data capture_dma_data; struct snd_dmaengine_dai_dma_data playback_dma_data; struct regmap *regmap; struct regmap *grf; bool has_capture; bool has_playback; /* * Used to indicate the tx/rx status. * I2S controller hopes to start the tx and rx together, * also to stop them when they are both try to stop. */ bool tx_start; bool rx_start; bool is_master_mode; const struct rk_i2s_pins *pins; unsigned int bclk_ratio; spinlock_t lock; /* tx/rx lock */ struct pinctrl *pinctrl; struct pinctrl_state *bclk_on; struct pinctrl_state *bclk_off; };
其中:
- dev:设备驱动模型中的device,可以将rk_i2s_dev看做其子类;
- hclk:I2S总线的时钟;
- mclk:I2S控制器的时钟,即i2s0模块输出的MCLK时钟(对应的时钟名称为clk_i2s0),是采样频率的256倍;
- capture_dma_data:capture dam data;
- playback_dma_data:playback dma data;
- lock:tx/rx 自旋锁;
- regmap:实现通过regmap机制来访问I2S0寄存器;
- grf:实现通过regmap机制来访问GRF寄存器;
- has_capture:表明是否有音频捕获功能;
- has_playback:表明是否有音频播放功能;
- pinctrl:保存client device的所有引脚状态;
- blck_on:保存blck_on引脚状态;
- blck_off:保存blck_off引脚状态;
3.2 struct rk_i2s_pins
struct rk_i2s_pins定义在sound/soc/rockchip/rockchip_i2s.c:
struct rk_i2s_pins { u32 reg_offset; u32 shift; };
四、相关API
4.1 i2s_pinctrl_select_bclk_off
i2s_pinctrl_select_bclk_off定义在sound/soc/rockchip/rockchip_i2s.c:
static int i2s_pinctrl_select_bclk_off(struct rk_i2s_dev *i2s) { int ret = 0; if (!IS_ERR(i2s->pinctrl) && !IS_ERR_OR_NULL(i2s->bclk_off)) ret = pinctrl_select_state(i2s->pinctrl, i2s->bclk_off); if (ret) dev_err(i2s->dev, "bclk disable failed %d\n", ret); return ret; }
这里调用pinctrl_select_state函数来配置设备引脚状态为bclk_off。更多pinctrl子系统知识可以参考:linux设备树-pinctrl子系统 。
4.2 rockchip_i2s_init_dai
rockchip_i2s_init_dai定义在sound/soc/rockchip/rockchip_i2s.c,主要是初始化dai playback/capuure stream,以及配置dai dma data;
static int rockchip_i2s_init_dai(struct rk_i2s_dev *i2s, struct resource *res, struct snd_soc_dai_driver **dp) { struct device_node *node = i2s->dev->of_node; struct snd_soc_dai_driver *dai; struct property *dma_names; const char *dma_name; unsigned int val; of_property_for_each_string(node, "dma-names", dma_names, dma_name) { // 获取属性"dma-names"的值,依次为"tx","rx" if (!strcmp(dma_name, "tx")) i2s->has_playback = true; // 设置标志位 if (!strcmp(dma_name, "rx")) i2s->has_capture = true; // 设置标志位 } dai = devm_kmemdup(i2s->dev, &rockchip_i2s_dai, // 动态分配内存,并将rockchip_i2s_dai内容拷贝到新分配的内存中,拷贝长度为sizeof(*dai) sizeof(*dai), GFP_KERNEL); if (!dai) return -ENOMEM; if (i2s->has_playback) { // 支持音频播放 // 配置pcm playback stream dai->playback.stream_name = "Playback"; dai->playback.channels_min = 2; // 最小通道数 dai->playback.channels_max = 8; // 最大通道数 dai->playback.rates = SNDRV_PCM_RATE_8000_192000; // 采样频率 dai->playback.formats = SNDRV_PCM_FMTBIT_S8 | // 音频格式 SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE; // 配置dai dma data,DMA client driver会使用到 i2s->playback_dma_data.addr = res->start + I2S_TXDR; // 偏移0x0024为发送FIFO数据寄存器 i2s->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; //音频采样位宽/8,单位字节 4字节 i2s->playback_dma_data.maxburst = 8; // FIFO深度 if (!of_property_read_u32(node, "rockchip,playback-channels", &val)) { // 获取属性"rockchip,playback-channels",值为8 if (val >= 2 && val <= 8) dai->playback.channels_max = val; } } if (i2s->has_capture) { // 支持录音 // 配置pcm capture stream dai->capture.stream_name = "Capture"; dai->capture.channels_min = 2; dai->capture.channels_max = 8; dai->capture.rates = SNDRV_PCM_RATE_8000_192000; dai->capture.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE; // 配置dai dma data,DMA client driver会使用到 i2s->capture_dma_data.addr = res->start + I2S_RXDR; i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; i2s->capture_dma_data.maxburst = 8; if (!of_property_read_u32(node, "rockchip,capture-channels", &val)) { // 获取属性"rockchip,capture-channels",值为8 if (val >= 2 && val <= 8) dai->capture.channels_max = val; } } if (dp) *dp = dai; return 0; }
这里主要就是动态分配一个struct snd_soc_dai_driver ,其内容是从rockchip_i2s_dai拷贝过去的,并根据dts设备节点属性进行调整;实际上函数执行完成之后得到的dai driver和rockchip_i2s_dai内容是一致的。
并在注册component时并将其作为devm_snd_soc_register_component的第三个参数,有关rockchip_i2s_dai咱们放到最后来说,因为这个比较重要。
4.3 devm_snd_soc_register_component
devm_snd_soc_register_component函数第二个参数为rockchip_i2s_component,定义在sound/soc/rockchip/rockchip_i2s.c:
ret = devm_snd_soc_register_component(&pdev->dev, // 9. 注册component &rockchip_i2s_component, dai, 1);
rockchip_i2s_component函数定义如下:
static const struct snd_soc_component_driver rockchip_i2s_component = { .name = DRV_NAME, // rockchip-i2s .legacy_dai_naming = 1, // 配置个这个之后,在注册component时创建的dai的名字设置为plat->dev的名称,平台设备是由i2s0设备节点转换来的;否则创建的dai的名字设置为dai driver的名字 };
这里并没有controls、dapm_widgets、dapm_routes相关的配置。
4.4 devm_snd_dmaengine_pcm_register
devm_snd_dmaengine_pcm_register定义在sound/soc/soc-devres.c,用于为设备注册一个dmaengine_pcm;
/** * devm_snd_dmaengine_pcm_register - resource managed dmaengine PCM registration * @dev: The parent device for the PCM device * @config: Platform specific PCM configuration * @flags: Platform specific quirks * * Register a dmaengine based PCM device with automatic unregistration when the * device is unregistered. */ int devm_snd_dmaengine_pcm_register(struct device *dev, const struct snd_dmaengine_pcm_config *config, unsigned int flags) // config传入NULL,flags传入0 { struct device **ptr; int ret; ptr = devres_alloc(devm_dmaengine_pcm_release, sizeof(*ptr), GFP_KERNEL); // 动态申请内存,指向一个struct *ptr if (!ptr) return -ENOMEM; ret = snd_dmaengine_pcm_register(dev, config, flags); // 申请DMA通道 if (ret == 0) { *ptr = dev; devres_add(dev, ptr); } else { devres_free(ptr); } return ret; }
4.4.1 snd_dmaengine_pcm_register
snd_dmaengine_pcm_register中通过dmaengine_pcm_request_chan_of去申请DMA通道,定义在sound/soc/soc-generic-dmaengine-pcm.c:
/** * snd_dmaengine_pcm_register - Register a dmaengine based PCM device * @dev: The parent device for the PCM device * @config: Platform specific PCM configuration * @flags: Platform specific quirks */ int snd_dmaengine_pcm_register(struct device *dev, const struct snd_dmaengine_pcm_config *config, unsigned int flags) { const struct snd_soc_component_driver *driver; struct dmaengine_pcm *pcm; int ret; pcm = kzalloc(sizeof(*pcm), GFP_KERNEL); // 动态分配一个dmaengine_pcm结构 if (!pcm) return -ENOMEM; #ifdef CONFIG_DEBUG_FS pcm->component.debugfs_prefix = "dma"; #endif if (!config) config = &snd_dmaengine_pcm_default_config; // 走这里 pcm->config = config; // 初始化pcm成员 pcm->flags = flags; ret = dmaengine_pcm_request_chan_of(pcm, dev, config); if (ret) goto err_free_dma; if (config->process) driver = &dmaengine_pcm_component_process; else driver = &dmaengine_pcm_component; // 走这里 ret = snd_soc_component_initialize(&pcm->component, driver, dev); // 初始化component if (ret) goto err_free_dma; ret = snd_soc_add_component(&pcm->component, NULL, 0); // 注册pcm component if (ret) goto err_free_dma; return 0; err_free_dma: dmaengine_pcm_release_chan(pcm); kfree(pcm); return ret; }
函数执行流程如下:
- 此处分配一个dmaengine_pcm结构,然后根据传入的config和flag设置pcm,config被设置为了snd_dmaengine_pcm_default_config;
- 调用dmaengine_pcm_request_chan_of函数获取dma的传输通道,根据传输的是否是半双工,设置pcm的通道;
- 调用snd_soc_add_platform函数注册platform component,实际上snd_soc_component_initialize和snd_soc_add_platform合在一起就是函数snd_soc_register_component;
snd_dmaengine_pcm_default_config定义如下:
static const struct snd_dmaengine_pcm_config snd_dmaengine_pcm_default_config = { .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, };
4.4.2 dmaengine_pcm_request_chan_of
dmaengine_pcm_request_chan_of根据flag 标志以及驱动中定义的数组获取DMA-name。为参数调用dma_request_slave_channel_reason;
static const char * const dmaengine_pcm_dma_channel_names[] = { [SNDRV_PCM_STREAM_PLAYBACK] = "tx", [SNDRV_PCM_STREAM_CAPTURE] = "rx", };
dmaengine_pcm_request_chan_of定义如下:
static int dmaengine_pcm_request_chan_of(struct dmaengine_pcm *pcm, struct device *dev, const struct snd_dmaengine_pcm_config *config) { unsigned int i; const char *name; struct dma_chan *chan; if ((pcm->flags & SND_DMAENGINE_PCM_FLAG_NO_DT) || (!dev->of_node && !(config->dma_dev && config->dma_dev->of_node))) return 0; if (config->dma_dev) { /* * If this warning is seen, it probably means that your Linux * device structure does not match your HW device structure. * It would be best to refactor the Linux device structure to * correctly match the HW structure. */ dev_warn(dev, "DMA channels sourced from device %s", dev_name(config->dma_dev)); dev = config->dma_dev; } for_each_pcm_streams(i) { if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX) name = "rx-tx"; else name = dmaengine_pcm_dma_channel_names[i]; if (config->chan_names[i]) name = config->chan_names[i]; chan = dma_request_chan(dev, name); if (IS_ERR(chan)) { /* * Only report probe deferral errors, channels * might not be present for devices that * support only TX or only RX. */ if (PTR_ERR(chan) == -EPROBE_DEFER) return -EPROBE_DEFER; pcm->chan[i] = NULL; } else { pcm->chan[i] = chan; } if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX) break; } if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX) pcm->chan[1] = pcm->chan[0]; return 0; }
4.4.3 dma_request_slave_channel_reason
dma_request_slave_channel_reason根据参数name并解析device tree 来申请具体的DMA 通道。一个DMA控制器有8个channel,每两个作为一组,既可以输出也可以输入,但它们的控制代码是一样的,并且在DMA注册过程中,已经使DMA处于可用状态了,这里根据传入的name 参数以及在device tree 中的描述申请具体的通道。
i2s0: i2s@ff880000 { dmas = <&dmac_bus 0>, <&dmac_bus 1>; dma-names = "tx", "rx"; ...... };
每个需要使用DMA的client都会通过 dmas来引用DMA控制器和通道,通过dma-names实现name的匹配。最终调用of_dma_request_slave_channel。
五、rockchip_i2s_dai
cpu dai和pcm配置信息通过结构体snd_soc_dai_driver描述,包括了dai的能力描述和操作接口,这里我们介绍一下rockchip_i2s_dai,其定义在sound/soc/rockchip/rockchip_i2s.c:
static struct snd_soc_dai_driver rockchip_i2s_dai = { .probe = rockchip_i2s_dai_probe, // 声卡注册的时候会为其创建一个类型为snd_soc_dapm_dai_in的playback dai widget,其name以及sname均设置为"Playback" .playback = { .stream_name = "Playback", .channels_min = 2, .channels_max = 8, .rates = SNDRV_PCM_RATE_8000_192000, // 支持的采样率 .formats = (SNDRV_PCM_FMTBIT_S8 | // 支持的位深度 SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE), }, // 声卡注册的时候会为其创建一个类型为snd_soc_dapm_dai_out的capture dai widget,其name以及sname均设置为"Capture" .capture = { .stream_name = "Capture", .channels_min = 1, .channels_max = 2, .rates = SNDRV_PCM_RATE_8000_192000, .formats = (SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE), }, .ops = &rockchip_i2s_dai_ops, .symmetric_rates = 1, };
其中:
- probe:component探测函数;在Macine驱动中会进行ASoC声卡的注册,其中会执行soc_probe_link_dais(card),即进行dai driver的探测工作,soc_probe_link_dais函数内部会调用dai->driver->probe(dai);
- name:cpu dai的名称标识,dai_link通过配置cpu dai_name来找到对应的cpu dai;
- capture:描述capture的能力;如回放设备所支持的声道数、采样率、音频格式;非常重要的字段;
- playback:描述playback的能力;如录制设备所支持声道数、采样率、音频格式;非常重要的字段;
- ops:cpu dai的操作函数集,这些函数集非常重要,用于dai的时钟配置、格式配置、硬件参数配置;
在注册ASoC声卡的时候,会调用soc_probe_link_components,每一个dai创建两个音频数据流的widget:
- 一个是类型为snd_soc_dapm_dai_in名称为Playback的播放流widget;
- 另一个是类型为snd_soc_dapm_dai_out名称为Capture的录音流widget;
在注册ASoC声卡的时候,会调用snd_soc_dapm_connect_dai_link_widgets,构建cpu dai和codec dai之间音频播放和录音的路径;
|-----------------| |-----------------| | | | | | Playback |----------------->| AIF1 Playback | 这俩Playback widget为snd_soc_dapm_dai_in类型的widget | | | | | Capture |<--------- -------| AIF1 Capture | 这俩Capture widget为snd_soc_dapm_dai_out类型的widget | | | | |-----------------| |-----------------| cpu_dai widget codec_dai widget
此时我们可以得到一个如下路径:
- 从cpu dai widget作为输入端的路径:Playback (位于platform驱动中,snd_soc_dapm_dai_in类型) --> AIF1 Playback(位于codec驱动中,snd_soc_dapm_dai_in类型) --> AIF1RX --> IF1 DAC --> IF1 DAC1 L -->.... -->HPO L Playback --> HPOL --> Headphones(位于machine驱动中) ;
- 以cpu dai widget作为输出端的路径:Capture(位于platform驱动中,snd_soc_dapm_dai_out类型) <-- AIF1 Capture(位于codec驱动中,snd_soc_dapm_dai_out类型)<-- AIF1TX <--IF1 ADC1 <-- .... <-- BST2 <-- IN2P <-- Mic Jack(位于machine驱动中) 。
5.1 rockchip_i2s_dai_probe
cpu dai driver的探测函数被设置为了rockchip_i2s_dai_probe,定义在sound/soc/rockchip/rockchip_i2s.c:
static int rockchip_i2s_dai_probe(struct snd_soc_dai *dai) { struct rk_i2s_dev *i2s = snd_soc_dai_get_drvdata(dai); snd_soc_dai_init_dma_data(dai, i2s->has_playback ? &i2s->playback_dma_data : NULL, i2s->has_capture ? &i2s->capture_dma_data : NULL); return 0; }
snd_soc_dai_init_dma_data定义在include/sound/soc-dai.h:
static inline void snd_soc_dai_init_dma_data(struct snd_soc_dai *dai, void *playback, void *capture) { snd_soc_dai_dma_data_set_playback(dai, playback); snd_soc_dai_dma_data_set_capture(dai, capture); }
宏snd_soc_dai_dma_data_set_playback、snd_soc_dai_dma_data_set_capture定义如下:
#define snd_soc_dai_dma_data_set_playback(dai, data) snd_soc_dai_dma_data_set(dai, SNDRV_PCM_STREAM_PLAYBACK, data) #define snd_soc_dai_dma_data_set_capture(dai, data) snd_soc_dai_dma_data_set(dai, SNDRV_PCM_STREAM_CAPTURE, data) #define snd_soc_dai_set_dma_data(dai, ss, data) snd_soc_dai_dma_data_set(dai, ss->stream, data)
这两个宏最终都是调用的snd_soc_dai_dma_data_set函数,用于设置dai->stream[stream].dma_data;
static inline void snd_soc_dai_dma_data_set(struct snd_soc_dai *dai, int stream, void *data) { dai->stream[stream].dma_data = data; }
5.2 rockchip_i2s_dai_ops
音频操作接口通过结构体struct snd_soc_dai_ops描述:
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
2018-07-30 第八节、图片分割之GrabCut算法、分水岭算法