手写简易操作系统(十七)--编写键盘驱动

前情提要

上一节我们实现了锁与信号量,这一节我们就可以实现键盘驱动了,访问键盘输入的数据也属于临界区资源,所以需要锁的存在。

一、键盘简介

之前的 ps/2 键盘使用的是中断驱动的,在当时,按下键盘就会触发中断,引导操作系统去处理这个按键行文。但是当今的usb键盘,使用的是轮询机制,cpu会定时访问键盘看有没有按下键盘。

我个人认为这是cpu技术的进步导致的,在之前,cpu的频率比较低,使用轮询可能会导致漏掉用户按键的行为。但是在今天,cpu的主频已经非常高了,处理一个按键行为就触发中断,这个开销太大了,而且轮询的频率也上来了,现在每秒访问几千次对电脑一点影响都没有,所以现在大多采用了轮询机制。

不过据说中断驱动的还是比较快,现在一些电竞主板还是支持ps/2的接口,这个未经论证。

1.1、键盘的通码与断码

键盘的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键按下时的编码叫做通码,键盘上的触电接通了电路,使硬件产生了一个编码,故此通码叫makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。

无论是按下键,或是松开键,当键的状态改变后,键盘中的8048芯片把按键对应的扫描码(通码或断码)发送到主板上的8042芯片,由8042处理后保存在自己的寄存器中,然后向8259A发送中断信号,这样处理器便去执行键盘中断处理程序,将8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

1.2、键盘扫描码

键的扫描码是由键盘中的键盘编码器决定的,不同的编码方案便是不同的键盘扫描码,也就是说,相同的键在不同的编码方案下产生的通码和断码也是不同的。

根据不同的编码方案,键盘扫描码有三套,分别称为scan code set 1、scan code set 2、scan code set 3。

其中scan code set 1是XT键盘用的扫描码,这个历史就比较久远了。scan code set 2是AT键盘的扫描码,这个键盘和我们当今的键盘也不是很一样,但是已经比较接近了。scan code set 3是IBM PS/2系列高端计算机所用的键盘上,IBM蓝色巨人现在都凉了,这个键盘也就很少看到了。

第二套键盘扫描码几乎是目前所使用的键盘的标准,因此大多数键盘向8042发送的扫描码都是第二套扫描码。但是难免有别的键盘,所以才会出现8042这个芯片,这个芯片做一个中间层,为了兼容第一套键盘扫描码对应的中断处理程序,不管键盘用的是何种键盘扫描码,当键盘将扫描码发送到8042后,都由8042转换成第一套扫描码,我们再从8042中读取扫描码。

这里我们给出常用键位的扫描码(这里的扫描码就是通码,加0x80就是断码)

按键扫描码按键扫描码按键扫描码
Esc0x01F10x3BF20x3C
F30x3DF40x3EF50x3F
F60x40F70x41F80x42
F90x43F100x44F110x57
F120x58PrintSc0x37ScrollLk0x46
Pause/Brk0x45`0x2910x02
20x0330x0440x05
50x0660x0770x08
80x0990x0A00x0B
-0x0C=0x0DBackspace0x0E
Tab0x0FQ0x10W0x11
E0x12R0x13T0x14
Y0x15U0x16I0x17
O0x18P0x19[0x1A
]0x1B|0x2BCapsLock0x3A
A0x1ES0x1FD0x20
F0x21G0x22H0x23
J0x24K0x25L0x26
;0x270x28Enter0x1C
Shift左0x2AZ0x2CX0x2D
C0x2EV0x2FB0x30
N0x31M0x32,0x33
.0x34/0x35Shift右0x36
Ctrl左0x1DWin左0xE0Alt左0x38
Space0x39Alt右0xE038Win右0xE0
Menu0xE0Ctrl右0xE01D

问:为什么会有通码和断码,通码不就够了嘛

**答:**如果按一个组合键的话,比如ctrl+a,是先按下ctrl,再按a,再松开ctrl,再松开a。如果没有断码,我们无法判断ctrl是否松开。

1.3、键盘的芯片

和键盘相关的芯片只有8042和8048,它们都是独立的处理器,都有自己的寄存器和内存。Intel 8048芯片或兼容芯片位于键盘中,它是键盘编码器,Intel 8042芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的IO接口,因此它是8048的代理,也是前面所得到的处理器和键盘的“中间层”。我们只需要学习8042就够了

他的端口如下

寄存器端口读写
Output Buffer(输出缓冲区)0x60
Input Buffer(输入缓冲区)0x60
Status Register(状态寄存器)0x64
Control Register(控制寄存器)0x64

状态寄存器8位宽度的寄存器,只读,反映8048和8042的内部工作状态。各位意义如下。

(1)位0:置1时表示输出缓冲区寄存器已满,处理器通过in指令读取后该位自动置0。
(2)位1:置1时表示输入缓冲区寄存器已满,8042将值读取后该位自动置0。
(3)位2:系统标志位,最初加电时为0,自检通过后置为1。
(4)位3:置1时,表示输入缓冲区中的内容是命令,置0时,输入缓冲区中的内容是普通数据。
(5)位4:置1时表示键盘启用,置0时表示键盘禁用。
(6)位5:置1时表示发送超时。
(7)位6:置1时表示接收超时。
(8)位7:来自8048的数据在奇偶校验时出错。

8位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式,意义如下。

(1)位0:置1时启用键盘中断。
(2)位1:置1时启用鼠标中断。
(3)位2:设置状态寄存器的位2。
(4)位3:置1时,状态寄存器的位4无效。
(5)位4:置1时禁止键盘。
(6)位5:置1时禁止鼠标。
(7)位6:将第二套键盘扫描码转换为第一套键盘扫描码。
(8)位7:保留位,默认为0。

二、环形队列

键盘中断的数据是放在队列中的,等待其他线程的读取。如果我们之前做过关于软件相关的工作,很容易理解这个概念,就是buffer,缓冲区。因为我们是一直在输入的,所以这里设计成了环形队列。

我们看一下环形队列的数据结构

#define bufsize 256/* 环形队列 */
struct ioqueue {// 生产者消费者问题struct lock lock;// 生产者,缓冲区不满时就继续往里面放数据struct task_struct* producer;// 消费者,缓冲区不空时就继续从往里面拿数据struct task_struct* consumer;char buf[bufsize];			// 缓冲区大小int32_t head;			    // 队首,数据往队首处写入int32_t tail;			    // 队尾,数据从队尾处读出
};

这个就很明朗了。一个生产者一个消费者,生产者向buf中添加数据,消费者从buf中取出数据,为了防止buf中的数据出错,生产者和消费者同时只能有一个可以访问到buf。如果buf中数据满了,生产者就不能放了,此时阻塞生产者,如果buf中数据为空,消费者就不能拿了,此时阻塞消费者。

我们看一下具体的实现

/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {lock_init(&ioq->lock);                 // 初始化io队列的锁ioq->producer = ioq->consumer = NULL;  // 生产者和消费者置空ioq->head = ioq->tail = 0;             // 队列的首尾指针指向缓冲区数组第0个位置
}/* 返回pos在缓冲区中的下一个位置值 */
static inline int32_t next_pos(int32_t pos) {return (pos + 1) % bufsize;
}/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {return next_pos(ioq->head) == ioq->tail;
}/* 判断队列是否已空 */
bool ioq_empty(struct ioqueue* ioq) {return ioq->head == ioq->tail;
}/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {// 二级指针不为空,指向的pcb指针地址为空ASSERT(*waiter == NULL && waiter != NULL);*waiter = running_thread();thread_block(TASK_BLOCKED);
}/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {// 二级指针指向不为空ASSERT(*waiter != NULL);thread_unblock(*waiter);*waiter = NULL;
}/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {// 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,等待生产者唤醒while (ioq_empty(ioq)) {lock_acquire(&ioq->lock);ioq_wait(&ioq->consumer);lock_release(&ioq->lock);}char byte = ioq->buf[ioq->tail];	  // 从缓冲区中取出ioq->tail = next_pos(ioq->tail);	  // 把读游标移到下一位置if (ioq->producer != NULL) {wakeup(&ioq->producer);		      // 唤醒生产者}return byte;
}/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {// 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,等待消费者线程唤醒自己while (ioq_full(ioq)) {lock_acquire(&ioq->lock);ioq_wait(&ioq->producer);lock_release(&ioq->lock);}ioq->buf[ioq->head] = byte;      // 把字节放入缓冲区中ioq->head = next_pos(ioq->head); // 把写游标移到下一位置if (ioq->consumer != NULL) {wakeup(&ioq->consumer);      // 唤醒消费者}
}

我们看一下后面两个函数,waitwakeup,这两个函数,这两个函数传入的是一个pcb指针的地址,所以这里是一个二级指针。所以无论是阻塞还是解除阻塞都是取这个二级指针的地址,也就得到了pcb指针。这里对于不熟悉指针的人来说可能会有点扰。

三、键盘驱动

#define KBD_BUF_PORT 0x60	 // 键盘buffer寄存器端口号为0x60/* 用转义字符定义部分控制字符 */
#define esc		    '\033'	 // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace	'\b'
#define tab		    '\t'
#define enter		'\r'
#define delete		'\177'	 // 八进制表示字符,十六进制为'\x7f'/* 以上不可见字符一律定义为0 */
#define char_invisible	0
#define ctrl_l_char	    char_invisible
#define ctrl_r_char	    char_invisible
#define shift_l_char    char_invisible
#define shift_r_char	char_invisible
#define alt_l_char	    char_invisible
#define alt_r_char	    char_invisible
#define caps_lock_char	char_invisible/* 定义控制字符的通码和断码 */
#define shift_l_make	0x2a
#define shift_r_make 	0x36 
#define alt_l_make   	0x38
#define alt_r_make   	0xe038
#define alt_r_break   	0xe0b8
#define ctrl_l_make  	0x1d
#define ctrl_r_make  	0xe01d
#define ctrl_r_break 	0xe09d
#define caps_lock_make 	0x3astruct ioqueue kbd_buf;	   // 定义键盘缓冲区/* 定义以下变量记录相应键是否按下的状态,* ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {/* 扫描码   未与shift组合  与shift组合*//* ---------------------------------- *//* 0x00 */	{0,	0},/* 0x01 */	{esc,	esc},/* 0x02 */	{'1',	'!'},/* 0x03 */	{'2',	'@'},/* 0x04 */	{'3',	'#'},/* 0x05 */	{'4',	'$'},/* 0x06 */	{'5',	'%'},/* 0x07 */	{'6',	'^'},/* 0x08 */	{'7',	'&'},/* 0x09 */	{'8',	'*'},/* 0x0A */	{'9',	'('},/* 0x0B */	{'0',	')'},/* 0x0C */	{'-',	'_'},/* 0x0D */	{'=',	'+'},/* 0x0E */	{backspace, backspace},/* 0x0F */	{tab,	tab},/* 0x10 */	{'q',	'Q'},/* 0x11 */	{'w',	'W'},/* 0x12 */	{'e',	'E'},/* 0x13 */	{'r',	'R'},/* 0x14 */	{'t',	'T'},/* 0x15 */	{'y',	'Y'},/* 0x16 */	{'u',	'U'},/* 0x17 */	{'i',	'I'},/* 0x18 */	{'o',	'O'},/* 0x19 */	{'p',	'P'},/* 0x1A */	{'[',	'{'},/* 0x1B */	{']',	'}'},/* 0x1C */	{enter,  enter},/* 0x1D */	{ctrl_l_char, ctrl_l_char},/* 0x1E */	{'a',	'A'},/* 0x1F */	{'s',	'S'},/* 0x20 */	{'d',	'D'},/* 0x21 */	{'f',	'F'},/* 0x22 */	{'g',	'G'},/* 0x23 */	{'h',	'H'},/* 0x24 */	{'j',	'J'},/* 0x25 */	{'k',	'K'},/* 0x26 */	{'l',	'L'},/* 0x27 */	{';',	':'},/* 0x28 */	{'\'',	'"'},/* 0x29 */	{'`',	'~'},/* 0x2A */	{shift_l_char, shift_l_char},/* 0x2B */	{'\\',	'|'},/* 0x2C */	{'z',	'Z'},/* 0x2D */	{'x',	'X'},/* 0x2E */	{'c',	'C'},/* 0x2F */	{'v',	'V'},/* 0x30 */	{'b',	'B'},/* 0x31 */	{'n',	'N'},/* 0x32 */	{'m',	'M'},/* 0x33 */	{',',	'<'},/* 0x34 */	{'.',	'>'},/* 0x35 */	{'/',	'?'},/* 0x36	*/	{shift_r_char, shift_r_char},/* 0x37 */	{'*',	'*'},/* 0x38 */	{alt_l_char, alt_l_char},/* 0x39 */	{' ',	' '},/* 0x3A */	{caps_lock_char, caps_lock_char}/*其它按键暂不处理*/
};/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */bool ctrl_down_last = ctrl_status;bool shift_down_last = shift_status;bool caps_lock_last = caps_lock_status;uint16_t scancode = inb(KBD_BUF_PORT);// 若扫描码是e0开头的, 结束此次中断处理函数,等待下一个扫描码进来if (scancode == 0xe0) {ext_scancode = true;    // 打开e0标记return;}// 如果上次是以0xe0开头,将扫描码合并if (ext_scancode) {scancode = ((0xe000) | scancode);ext_scancode = false;   // 关闭e0标记}// 若是断码(按键弹起时产生的扫描码)if ((scancode & 0x0080) != 0) {// 获得相应的通码uint16_t make_code = (scancode &= 0xff7f);// 若是任意以下三个键弹起了,将状态置为falseif (make_code == ctrl_l_make || make_code == ctrl_r_make) {ctrl_status = false;}else if (make_code == shift_l_make || make_code == shift_r_make) {shift_status = false;}else if (make_code == alt_l_make || make_code == alt_r_make) {alt_status = false;}// 若是其他非控制键位,不需要处理,那些键位我们只需要知道通码return;}// 若是通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_codeelse if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)) {// keymap的二维索引bool shift = false;// 按下的键不是字母if ((scancode < 0x0e) || (scancode == 0x29) || \(scancode == 0x1a) || (scancode == 0x1b) || \(scancode == 0x2b) || (scancode == 0x27) || \(scancode == 0x28) || (scancode == 0x33) || \(scancode == 0x34) || (scancode == 0x35)) {  if (shift_down_last) {shift = true;}}// 如果按下的键是字母,需要和CapsLock配合else {if (shift_down_last && caps_lock_last) {      // 如果shift和capslock同时按下shift = false;}else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下shift = true;}else {shift = false;}}// 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.uint8_t index = (scancode &= 0x00ff);// 在数组中找到对应的字符char cur_char = keymap[index][shift];// 如果cur_char不为0,也就是ascii码为除'\0'外的字符就加入键盘缓冲区中if (cur_char) {// 如果ctrl按下,且输入的字符为‘l’或者‘u’,那就保存为 cur_char-‘a’,主要是‘a’前面26位没啥用if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {cur_char -= 'a';}// 如果缓冲区未满,就将其加入缓冲区if (!ioq_full(&kbd_buf)) {ioq_putchar(&kbd_buf, cur_char);}return;}// 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键if (scancode == ctrl_l_make || scancode == ctrl_r_make) {ctrl_status = true;}else if (scancode == shift_l_make || scancode == shift_r_make) {shift_status = true;}else if (scancode == alt_l_make || scancode == alt_r_make) {alt_status = true;}// 这里注意,大写的锁定键是取反else if (scancode == caps_lock_make) {caps_lock_status = !caps_lock_status;}}else {put_str("unknown key\n");}
}/* 键盘初始化 */
void keyboard_init() {put_str("keyboard init start\n");ioqueue_init(&kbd_buf);register_handler(0x21, intr_keyboard_handler);put_str("keyboard init done\n");
}

键盘驱动就稍显复杂一点,主要是涉及到了shiftctrlaltcaplock这些个控制键,这些键位是否按下所表示的通码断码是不一样的。这里就是处理字符,相信大家看代码就可以看明白。

四、仿真

我们创建一个线程,键盘输入什么,打印什么

image-20240325171327961

结束语

本节我们编写了键盘驱动以及其使用的环形队列数据结构。下一节我们将实现一个用户进程,即特权级为3的进程。

老规矩,代码地址为 https://github.com/lyajpunov/os

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/293029.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Abaqus周期性边界代表体单元Random Sphere RVE 3D (Mesh)插件

插件介绍 Random Sphere RVE 3D (Mesh) - AbyssFish 插件可在Abaqus生成三维具备周期性边界条件(Periodic Boundary Conditions, PBC)的随机球体骨料及骨料-水泥界面过渡区(Interfacial Transition Zone, ITZ)模型。即采用周期性代表性体积单元法(Periodic Representative Vol…

1.8 python 模块 time、random、string、hashlib、os、re、json

ython之模块 一、模块的介绍 &#xff08;1&#xff09;python模块&#xff0c;是一个python文件&#xff0c;以一个.py文件&#xff0c;包含了python对象定义和pyhton语句 &#xff08;2&#xff09;python对象定义和python语句 &#xff08;3&#xff09;模块让你能够有逻辑地…

Cookie 与 Session

目录 一、获取Cookie/Session 1、理解Cookie 2、理解Session 3、Cookie 和 Session 的区别 4、获取Cookie 4.1 传统获取Cookie 4.2 简洁获取Cookie 5、Session 存储和获取 5.1 Session存储 5.2 Session读取 5.3 简洁获取 Session 一、获取Cookie/Session 1、理解Co…

【Linux】详解软硬链接

一、软硬链接的建立方法 1.1软链接的建立 假设在当前目录下有一个test.txt文件&#xff0c;要对其建立软链接&#xff0c;做法如下&#xff1a; ln就是link的意思&#xff0c;-s表示软链接&#xff0c;test.txt要建立软链接的文件名&#xff0c;后面跟上要建立的软链接文件名…

k8s1.28.8版本配置Alertmanager报警方式(邮件,企业微信)

文章目录 总结部署流程 Alertmanager 三大核心1. 分组告警2. 告警抑制3. 告警静默 报警过滤静默通知方案一&#xff1a;方案二&#xff1a; 抑制报警规则案例一 参考文档 自定义路由告警&#xff0c;分来自不同路由的告警&#xff0c;艾特不同的人员进行区分修改 alertmanager …

linux C:变量、运算符

linux C 文章目录 变量运算符 一、变量 [存储类型] 数据类型 标识符 值 标识符&#xff1a;由数字、字母、下划线组成的序列&#xff0c;不能以数字开头。 数据类型&#xff1a;基本数据类型构造类型 存储类型&#xff1a;auto static…

4月深圳振威新能源产业博览会丨千万订单采购对接会!

4月深圳振威新能源产业博览会丨千万订单采购对接会&#xff01; 目前&#xff0c;振威新能源海外招商团队已成功与俄罗斯、泰国多家组织机构建立合作联系&#xff01;已确定携多家知名企业到现场采购&#xff01; 电池与储能 同时&#xff0c;振威新能源团队也成功与泰国储能技…

【KingSCADA】播放语音

1.函数介绍 PlaySound(string strWaveFileName, int nMode);下面是官方帮助文档中的解释&#xff1a; 2.生成语音文件 3.使用脚本播放音频文件 将音频文件存放在工程目录下面&#xff0c;我存放在了…\Resources\文件夹下&#xff1a; 我简单的写了一个定时1分钟播放一次语…

Docket常见的软件部署1

1 安装MySQL # 查看MySQL镜像 docker search mysql # 拉起镜像 docker pull mysql:5.7 # 创建MySQL数据映射卷&#xff0c;防止数据不丢失 mkdir -p /hmoe/tem/docker/mysql/data/ # 启动镜像 docker run -d --name mysql -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -v /home…

蓝桥杯第七届大学B组详解

目录 1.煤球数量&#xff1b; 2.生日蜡烛&#xff1b; 3.凑算式 4.方格填数 5.四平方和 6.交换瓶子 7.最大比例 1.煤球数量 题目解析&#xff1a;可以根据题目的意思&#xff0c;找到规律。 1 *- 1个 2 *** 3个 3 ****** 6个 4 ********** 10个 不难发现 第…

OSCP靶场--Internal

OSCP靶场–Internal 考点(CVE-2009-3103) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.216.40 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-31 07:00 EDT Nmap scan report for 192.168.216.40 Host is up…

C++11新特性(二):更好用的 lambda 表达式和 function 包装器

目录 lambda 表达式 基本格式及参数列表 对于 lambda 捕捉列表的说明 function 包装器 bind 包装器 lambda 表达式 C11引入了lambda表达式&#xff0c;它是一种用于创建匿名函数的语法。lambda表达式可以被视为一个匿名函数对象&#xff0c;它可以在需要函数对象的地方使用…

PyTorch 教程-快速上手指南

文章目录 PyTorch Quickstart1.处理数据2.创建模型3.优化模型参数4.保存模型5.加载模型 PyTorch 基础入门1.Tensors1.1初始化张量1.2张量的属性1.3张量运算1.3.1张量的索引和切片1.3.2张量的连接1.3.3算术运算1.3.4单元素张量转变为Python数值 1.4Tensor与NumPy的桥接1.4.1Tens…

系统慢查询的思考

系统慢查询的思考 在一个系统中发现慢查询的功能或很卡的现象。你是怎么思考的&#xff1f;从哪几个方面去思考&#xff1f;会用什么工具&#xff1f; 一个系统使用了几年后都可能会出现这样的问题。原因可能有以下几点。 数据量的增加。系统中平时的使用中数据量是有一个累…

【AXIS】AXI-Stream FIFO设计实现(四)——异步时钟

前文介绍了几种同步时钟情况下的AXI Stream FIFO实现方式&#xff0c;一般来说&#xff0c;FIFO也需要承担异步时钟域模块间数据传输的功能&#xff0c;本文介绍异步AXIS FIFO的实现方式。 如前文所说&#xff0c;AXI-Stream FIFO十分类似于FWFT异步FIFO&#xff0c;推荐参考前…

MIPI CSI-2 Low Level Protocol解读

一、Low Level Protocol介绍 LLP 是一种面向字节的基于数据包的协议&#xff0c;支持使用短数据包和长数据包格式传输任意数据。为简单起见&#xff0c;本节中的所有示例均为单通道配置。 LLP特性&#xff1a; 传输任意数据&#xff08;与有效载荷无关&#xff09; 8 位字大…

Chatgpt掘金之旅—有爱AI商业实战篇(二)

演示站点&#xff1a; https://ai.uaai.cn 对话模块 官方论坛&#xff1a; www.jingyuai.com 京娱AI 一、前言&#xff1a; 成为一名商业作者是一个蕴含着无限可能的职业选择。在当下数字化的时代&#xff0c;作家们有着众多的平台可以展示和推广自己的作品。无论您是对写书、文…

MSPF5438数据卫星透传

最近在网上找了个项目来做&#xff0c;实现功能简单描述就是通过Lora模块E30-170T27D接收上位机发送的数据包&#xff0c;并对接收数据包进行正确性校验&#xff0c;若数据包校验成功则将其储存在W25Q125FV 中&#xff0c;待上位机发送数据包传输完毕指令后&#xff0c;单片机启…

Docker配置Mysql

1.首页搜索mysql镜像 2.选择对应版本的MySQL&#xff0c;点击pull 3.pull完成以后&#xff0c;点击images&#xff0c;这里可以看到刚刚pull完成的mysql版本 4.打开命令界面&#xff0c;运行命令 docker images ,查看当前已经pull的images 5.运行命令设置mysql docker run -it…

【有芯职说】数字芯片BES工程师

一、 数字芯片BES工程师简介 今天来聊聊数字芯片BES工程师&#xff0c;其中BES是Back End Support的缩写&#xff0c;就是后端支持的意思。其实这个岗位是数字IC前端设计和数字IC后端设计之间的一座桥&#xff0c;完成从寄存器传输级设计到具体工艺的mapping和实现。这个岗位在…