自己动手编写嵌入式Bootloader之(2)

自己动手编写嵌入式Bootloader之(1) 阅读详情

第二部分:通过网口下载内核映像

要实现通过网口下载文件的功能,从底层到上层需要做的工作包括:开发板上的网卡芯片的驱动程序;TCP/IP协议栈的实现;TFTP客户端应用程序的实现。我们使用的OK2440开发板配备CS8900A网卡芯片。 为了简单起见,网络数据包的发送和接收都使用轮询方式,不使用中断;协议栈只使用ARP/IP/UDP协议,不涉及TCP及其他协议;应用程序只实现最简单的TFTP客户端。

1. 全局配置信息

发送和接收的数据缓冲区,使用全局静态缓冲区,不使用动态内存分配。第一阶段运行结束之后,CPU内部4KB的SteppingStone可以用作其它用途,我们就用它做网络数据接收、发送的缓冲区。亦可用作标准输入输出的缓冲区。
unsigned char *TxBuf = (unsigned char *)0;
unsigned char *RxBuf = (unsigned char *)1024;

使用若干个全局变量来保存网络配置信息:
unsigned char    NetOurEther[6] =            /* Our ethernet address        */
        {0x00, 0x09, 0x58, 0xD8, 0x11, 0x22};
开发板的MAC地址,这个是任意设置的。

unsigned char    NetServerEther[6] =            /* Boot server enet address    */
    {0x00, 0x14, 0x2A, 0xA5, 0x50, 0x97};
服务器也就是主机的MAC地址,这个要跟主机MAC一致,可以在主机上运行ifconfig命令查到。

unsigned long    NetOurIP = 0xC0A801FC;        /* Our IP addr 192.168.1.252    */
unsigned long    NetServerIP = 0xC0A801F9;       /* Server IP   192.168.1.249    */
网络协议中IP地址一般是用一个4字节整型数表示的。

2. CS8900A以太网驱动程序


硬件电路决定了CS8900的物理地址是在BANK3的区间内,CS8900是16位的寄存器,故我们设置BANK3的BUS WIDTH也为16位。设置BANK3: 总线宽度16,使能nWait,使能UB/LB
BANKCON3:0x1F7C                                                                                                                                                                                                                                                                                              

网卡CS8900的访问基址为0x19000000,之所以再偏移0x300是由它的特性决定的
#define CS8900_BASE 0x19000300

CS8900 读写寄存器的方式有些特别。要读一个寄存器,先向CS8900_PPTR中写入该寄存器地址,再从CS8900_PDATA中读出该寄存器值;要写一个寄存器,先向CS8900PPTR中写入该寄存器地址,再向CS8900_PDATA中写入要写入的值。不管是寄存器地址还是要读写的数值,都是16位的,也就是说都是unsigned short类型的。因此,读写寄存器的函数如下:
static unsigned short get_reg (int regno)
{
    CS8900_PPTR = regno;
    return CS8900_PDATA;
}
 
static void put_reg (int regno, unsigned short val)
{
    CS8900_PPTR = regno;
    CS8900_PDATA = val;
}

读芯片ID: CS8900的芯片ID存放在PP_ChipID寄存器中,读该寄存器得到的正确值应该是0x630E,这可以初步判断一些地址/引脚的设置是否正确,如果读出的不是0x630E,那么CS8900肯定不能正常工作。
设置MAC地址:
 
MAC地址并不是固定的,可以由我们随意设置。从寄存器PP_IA开始的6个字节存放MAC地址。比如下面的代码把MAC地址设为 00 09 58 D8 11 22:
    put_reg (PP_IA + 0, 0x00 | 0x09 << 8);
    put_reg (PP_IA + 2, 0x58 | 0xD8 << 8);
    put_reg (PP_IA + 4, 0x11 | 0x22 << 8);

因为是Little Endian, 所以0x09<<8, 但是在寄存器内存中还是 0x00放在前面。

 
寄存器初始化: 设置CS8900的工作模式
    /* 只接收目标地址为本网卡的无错误数据包 */
    put_reg (PP_RxCTL, PP_RxCTL_IA | PP_RxCTL_Broadcast | PP_RxCTL_RxOK);
    /* 当进行接收操作时,不要产生任何中断 */
    put_reg (PP_RxCFG, 0);
    /* 当进行发送操作时,不要产生任何中断 */
    put_reg (PP_TxCFG, 0);
    /* 当进行缓存操作时,不要产生任何中断 */
    put_reg (PP_BufCFG, 0);
    /* 使能发送和接收模式 */
    put_reg (PP_LineCTL, PP_LineCTL_Rx | PP_LineCTL_Tx);

发送数据包:
int eth_send (volatile void *packet, int length)
两个参数:要发送的数据包首地址、长度
 
TxCMD 和TxLen寄存器用来初始化数据包的发送,其具体含义见CS8900数据手册第70页。这里PP_TxCmd_TxStart_Full被定义为 0x00C0,表示直到整个数据侦都加载到CS8900内部缓存之后才开始发送,数据侦的长度为CS8900_TxLEN.
/* initiate a transmit sequence */
    CS8900_TxCMD = PP_TxCmd_TxStart_Full;
    CS8900_TxLEN = length;

使用TxCMD下达发送数据的命令后,再读取 PP_BusSTAT 总线状态寄存器判断是否做好发送数据的准备。当get_reg (PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零时表示可以发送了。 使用一个循环进行实际的发送操作:

for (addr = packet; length > 0; length -= 2)
        {
            CS8900_RTDATA = *addr++;
        }

这里 addr 也是unsigned short类型的指针, 每次向CS8900_RTDATA写入两个字节数据。这里假设要发送的数据包长度为偶数。

最后,通过读取PP_TER寄存器可以知道是否发送完毕,是否发送成功。

接收数据包:

首先,通过读取PP_RER寄存器判断是否接收到数据。如果接收到数据,则连续两次读取 CS8900_RTDATA 的值,
    status = CS8900_RTDATA;        /* stat */
    rxlen = CS8900_RTDATA;        /* len */
rxlen 为接收到的数据长度。
然后用一个循环连续读取 rxlen 长度的数据:

for (addr = (unsigned short *) &RxBuf[0], i = rxlen >> 1; i > 0;
         i--)
        *addr++ = CS8900_RTDATA;
    if (rxlen & 1)
        *addr++ = CS8900_RTDATA;

其中 RxBuf 为预先在内存中开辟的一块接收缓冲区。 每次循环读取两个字节,还需要处理长度为奇数的情况。

最后,把RxBuf交给上层的协议处理:net_receive( &RxBuf[0], rxlen );

3. Ethernet MAC层协议的实现

上层的数据包(如IP包、ARP包)到来时,需要添加一个14字节的MAC头, 然后再交给网卡发送出去。 MAC头包含目的MAC地址、源MAC地址、协议类型三个字段。如下图所示。数据包末尾的CRC校验我们不使用。


使用下面的代码填充MAC头。其中协议类型,对IP为0x0800, 对ARP为0x0806

    struct mac_header *p = (struct mac_header*)(buf);
    memcpy (p->dest, NetServerEther, 6);
    memcpy (p->src, NetOurEther, 6);
    p->proto = htons(proto);

4. ARP协议的实现


      一般的方式是建立一个全局的ARP映射缓存表,随着系统的运行不断查找、更新该表。但是我们要完成的功能仅仅是从TFTP服务器下载内核和文件系统映像,而服务器的IP和MAC地址都是固定的,因此可以简化ARP映射表,只用两个变量分别保存服务器IP和MAC,再用两个变量保存开发板IP和MAC即可。并且更新映射表的功能也可以省略,只在系统初始化时把这四个地址都设置好,使用过程中不会发生改变,所以不需要更新。这样,我们的ARP协议只需要完成接受ARP请求、发送ARP应答的功能,而发送ARP请求和接受ARP应答的功能可以省略,这样大大简化了协议栈的设计。
    按照维基百科上的介绍(http://en.wikipedia.org/wiki/Address_Resolution_Protocol),ARP 是一个数据链路层协议,(我感觉它应该是网络层的协议),它的作用是在只知道一个主机网络层IP地址的情况下找到它的硬件地址。在以太网上,它主要用来把 IP地址转换为以太网MAC地址。由于是链路层协议,ARP的作用范围仅限于本地局域网。
 
    ARP数据包长度为28字节,其中各字节的含义如下图所示:


对各个段作简单的解释:
Hardware type (HTYPE)  每个数据链路层协议都被分配到一个数,比如,Ethernet 是 1
Protocol type (PTYPE)  在这个域,每个网络层协议都被分配到一个数(标号),比如,IP是0x0800
Hardware length (HLEN)  硬件地址的长度。以太网Ethernet的MAC地址长度是6个字节
Protocol length (PLEN)  维基上写的是“逻辑地址”的长度,其实也就是网络层地址的长度。IPv4地址的长度为4个字节。
Operation  表明发送者的操作,也就是数据包的类型:1表示ARP请求;2表示ARP回应;3表示RARP请求;4表示RARP回应。
Sender hardware address (SHA)  发送者的硬件地址
Sender protocol address (SPA)  发送者的协议地址,也就是发送者IP地址。
Target hardware address (THA)  目标接收者的硬件MAC地址。如果是ARP请求,这个域被忽略。
Target protocol address (TPA)  目标接收者的IP地址。

知道了包结构,我们就可以设计一个结构体:

struct arp_header{
    unsigned short        ar_hrd;        /* Format of hardware address    */
    unsigned short        ar_pro;        /* Format of protocol address    */
    unsigned char        ar_hln;     /* Length of hardware address    */
    unsigned char        ar_pln;     /* Length of protocol address    */
    unsigned short        ar_op;        /* Operation            */

    unsigned char        ar_sha[6];    /* Sender hardware address    */
    unsigned long        ar_spa;     /* Sender protocol address    */
    unsigned char        ar_tha[6];    /* Target hardware address    */
    unsigned long        ar_tpa;     /* Target protocol address    */
}__attribute__ ((packed));

属性 __attribute__((packet)) 告诉编译器使用紧缩方式存放结构体内容(1 Byte align), 不使用默认的4字节对齐, 这样就不会产生冗余字节。此时的 sizeof(struct arp_header) = 28。 如果不加packed属性, 运行 sizeof(struct arp_header) 得到 32, 而不是 28。 数据段就产生了错位。

前面已经说过,我们只实现接收ARP请求并发送ARP应答的功能,因此只用一个简单的函数就可实现:

static int arp_handle( unsigned char *buf, unsigned int len )
{
    struct arp_header *pRx, *pTx;
    pRx = (struct arp_header *)(buf);
    pTx = (struct arp_header *)&TxBuf[256];

    switch (htons(pRx->ar_op))
    {
        case ARP_REQUEST:
            if (pRx->ar_tpa == htonl(NetOurIP))
            {
                pTx->ar_hrd = htons(0x01);
                pTx->ar_pro = htons(PROTO_IP);
                pTx->ar_hln = 0x06;
                pTx->ar_pln = 0x04;
                pTx->ar_op = htons(ARP_REPLY);
                memcpy(pTx->ar_sha, NetOurEther, 6);
                pTx->ar_spa = htonl(NetOurIP);
                memcpy (pTx->ar_tha, pRx->ar_sha, 6);      
                pTx->ar_tpa = pRx->ar_spa;
                mac_send( (unsigned char*)pTx, sizeof(struct arp_header), PROTO_ARP);
            }
            break;
        case ARP_REPLY:
            printf("/n/rGot ARP reply/n");
            break;
        default:
            printf("/n/r ar_op Not Support./n");
            break;
    }
    return 0;
}

接收到的数据保存在pRx地址处,要发送的数据地址指定为pTx位于发送缓冲区中。如果接收到的是ARP请求包并且IP地址也符合,则在pTx处构造一个ARP应答包并交给mac_send()发送出去。

5. IP协议的实现

IP数据包的格式如下表所示:

+
Bits 0–3
4–7
8–15
16–18
19–31
0
Version
Header length
Type of Service
Total Length
32
Identification
Flags
Fragment Offset
64
Time to Live
Protocol
Header Checksum
96
Source Address
128
Destination Address
160
Options
160 or 192+
Data
IP协议的简化:IP协议在网络中主要完成路由选择和网络分段的功能。起始Bit 0-3表示版本号,对IPv4来说取值为4即0100即可。Header length域指明IP数据包header的长度(不包括数据Data域),以四字节为单位,因为Options域是可选的所以IP Header的长度并不固定。我们不使用Option域,所以取最小值5,表示Header长度为20字节。服务类型域(Type of Service, TOS)是为特殊的应用如VoIP等保留的,我们不使用,赋值为零即可。接下来2个字节的Total Length域表示整个数据包的长度,包括Header和Data,以字节为单位。 标识域(Identification)用来给数据包一个唯一的编号,用于验证和跟踪等,我们不使用,直接赋值为零即可。Flags和Offset用于分段包的重组,我们不使用,把Flags的第2位设为1表示是不可分段的,Offset赋值为零即可。生存时间(Time to Live, TTL)表示该数据包在网络上的有效期,我们简单的把它设为最大值0xFF即可。协议域(Protocol)表示传输层使用什么协议,RFC790文档为每个协议都规定了唯一的编号,如UDP编号为17。Header Checksum为Header区域的校验和,在校验之前该域初始为0,然后计算整个头部的校验和,把结果存放在该域,计算校验的方法是把头部看成以16位为单位的数字组成,依次进行二进制反码求和。接下来的八个字节是源IP地址和目的IP地址,没什么可说的。
综上所述,我们只保留了IP协议中必须的关键字段,因而简化了设计,对IP数据包进行填充的代码段如下:
    struct ip_header *p = (struct ip_header*)(buf);
    p->ver_ihl = 0x45;                  // 1 Byte
    p->tos = 0x00;                      // 1 Byte
    p->tlen = htons(len);               // 2 Byte
    p->identification = htons(0x00);    // 2 Byte
    p->flags_fo = htons(0x4000);        // 2 Byte
    p->ttl = 0xFF;                      // 1 Byte
    p->proto = 17;                      // 1 Byte, 17 for UDP
    p->ip_src = htonl(NetOurIP);        // 4 Byte
    p->ip_dest = htonl(NetServerIP);    // 4 Byte
    p->crc = 0x0;                       // 2 Byte, To be
    p->crc = checksum( buf, sizeof(struct ip_header) );

CheckSum 校验和:
IP,TCP,UDP等许多协议的头部都设置了校验和项,它们采用的算法是一样的,将被校验的数据按16位进行划分(若数据字节长度为奇数,则在数据尾部补一个字节0),对每16位求反码和,然后再对和取反码。 代码如下:

unsigned short checksum(unsigned char *ptr, int len)
{
    unsigned long sum = 0;
    unsigned short *p = (unsigned short *)ptr;
    while (len > 1)
    {
        sum += *p++;
        len -= 2;
    }
    if(len == 1)
        sum += *(unsigned char *)p;
    while(sum>>16)
        sum = (sum&0xffff) + (sum>>16);
    return (unsigned short)((~sum)&0xffff);
}


6. UDP协议的实现

Source PortDestination Port
LengthChecksum
 
Data
 
       在传输层我们抛弃了复杂的TCP协议而使用简单的UDP协议。虽然UDP是无连接的协议,它不保证数据包一定能够到达目的主机,但是在嵌入式开发中,开发板跟主机通常位于同一内部局域网内,网络环境良好,数据丢失的可能性很小,并且UDP容易实现,占用资源小,因此更适合于嵌入式环境。 UDP头部包含了可选的校验和字段,而校验要涉及到伪报头,为了简化设计和减小开销,我们不使用校验,直接把该字段设为零,表示不使用校验。UDP包填充代码如下:
    struct udp_header *P = (struct udp_header*)(buf);
    P->port_src = htons(0x8DA4); // 2 Byte
    P->port_dest = htons(port);  // 2 Byte
    P->tlen = htons(len);        // 2 Byte
    P->crc = 0x00;               // Do Not Checksum, 2 Byte

关于源端口号和目的端口号的设定,在TFTP实现时会详细说明。

7. TFTP客户端的实现

tftp是一个很简单的文件传输协议,在传输层使用UDP协议。它有四种类型的包: 读请求RRQ包,DATA包,ACK包,ERROR包,每个包的前两个字节Opcode指定包的类型。(RRQ用于请求下载,WRQ用于请求上传,我们只用到RRQ)。



下载文件的过程分析如下: 客户端(A)从任意端口X向服务器(S)的端口69发送一个RRQ包,该包中指明了要求下载的文件名;服务器(S)找到该文件,读取文件内容组成DATA包,从任意端口Y向客户端(A)的端口X发送这个DATA包,第一个DATA包编号为1;从此以后,客户端确定使用端口X,服务器确定使用端口Y, 客户端向服务器发送ACK包,编号为1。服务器接到编号为1的ACK包之后,发送第二个DATA包,如此继续下去。
怎样判断传输结束呢? 按照规定,DATA包中的数据段为512字节, 如果小于512字节,表示这是最后一个DATA包,文件已传输完毕。


(R1) Host A requests to read

(R2) Server S sends data packet 1
(R3) Host A acknowledges data packet 1

注意在这个过程中端口的变化。开始RRQ是69,但是DATA和ACK都不是使用69,而是使用另外一个随机的端口。 服务器在接到RRQ后,不返回任何回应信息,直接发送第一个DATA包,而且DATA包编号从1开始,而不是从0开始。

编程时为简单起见,客户端使用了固定的端口号X=0x8DA4,服务器端口号Y是随机的,只能通过解析UDP数据包获得。

 
int tftp_download(unsigned char *addr, const char *filename)
{
    int i=0;
    unsigned short curblock = 1;

    tftp_send_request( &TxBuf[256], filename );
    msdelay(100);

    while (1)
    {
        eth_rx();
      
        if( pGtftp == NULL )
            continue;
        
        if ( ntohs(pGtftp->opcode) == TFTP_DATA )
        {
            if (ntohs(pGtftp->u.blocknum) == curblock)
            {
                printf("/r Current Block Number = %d", curblock);
                for (i=0; i<iGLen-4; i++)
                {
                    *(addr++) = *(pGtftp->data+i);
                }
                tftp_send_ack( &TxBuf[256], curblock);
                
                if (iGLen < TFTP_DATASIZE+4)
                {
                    break;
                }
                curblock += 1;
            }
            else if (ntohs(pGtftp->u.blocknum) < curblock)
            {
                tftp_send_ack( &TxBuf[256], ntohs(pGtftp->u.blocknum));
            }
            else
            {
                printf("/n/rBlock Number Not Match.");
                printf("Block Number = %d, curblock = %d/n", ntohs(pGtftp->u.blocknum), curblock);  
            }
        }
        else if ( ntohs(pGtftp->opcode) == TFTP_ERROR )
        {
            switch( ntohs(pGtftp->u.errcode) )
            {
               // 此处省略
            }
        }
        else if ( ntohs(pGtftp->opcode) == TFTP_RRQ )
        {}// 此处省略若干 else if
       
        pGtftp = NULL;
        iGLen = 0;
    }
    
    printf("/n/rTransfer complete: %d Bytes./n/r", (curblock-1)*TFTP_DATASIZE + iGLen-4 );
    
    return 0;
}
 
工业传感器与变送器详解:11 HART智能仪表 HART智能仪表的核心是:4–20 mA + FSK数字通信 → 智能仪表。:实现模拟测量与数字通信共存。HART通信:实现仪表远程访问。:建立多变量仪表数据模型。参数配置:实现远程组态。诊断:实现设备健康管理。最终工业感知链:物理量 → 传感器 → 智能变送器 → 4–20 mA + HART → 数字仪表管理 → DCS控制系统。 阅读详情

相关推荐

Pspice开关电源仿真:从零实现反激式电源设计实战案例

通过Pspice仿真工具,手把手实现反激式开关电源的设计与优化,深入剖析关键参数设置与波形分析,帮助掌握Pspice在电源开发中的实际应用技巧。

weixin_35756690的博客 1104

系统移植的四大步骤

最近在学习系统移植的相关知识,在学习和调试过程中,发现了很多问题,也解决了很多问题,但总是对于我们的开发结果有一种莫名其妙的感觉,纠其原因,主要对于我们的开发环境没有一个深刻的认识,有时候几个简单的命令就可以完成非常复杂的功能,可是我们有没有想过,为什么会有这样的效果?如果没有去追问,只是机械地完成,并且看到实验效果,这样做其实并没有真正的掌握系统移植的本质。在做每一个步骤的时候,首先问问自己...

高明的专栏 2795

2021电赛A题:让信号“现原形“的失真度测量,国一队用一片VCA821搞定(2026-08-19)

让你做一个**“信号瑕疵检测仪”**——给一个被污染的周期信号(带 5%~50% 失真),你在 10 秒内测出它"歪"了多少,并把基波 + 5 次谐波"画"到屏幕上。

飞多学堂的博客 11

嵌入式应用中的数据校验

为保证嵌入式应用中数据通信的准确性,通常需要对传输的数据进行校验。 这里介绍一种累加和校验算法的实现。 发送方将所有目标数据进行累加求和,对求和结果取反作为校验值,连同目标数据一起发送给接收方。 接收方将接收到含有校验和数据进行累加求和,求和结果加一为零则说明数据传输正确。 算法原理是按位取反后的数据再与原来的数相加会把所有位都变为1,此时再把相加结果加一就会导致数据溢出,此时相加结果的所有数据位都会变为0。 /* * 串口通信常用的几种数据校验和方式 * By 羽墨志 */ #include &l

羽墨志 655

can收发程序分析

<br />  对于发送程序,首先需要按照can协议,根据报文的格式,将can控制器的发送缓冲区填充完毕。然后再启动发送命令,具体的发送程序如下所示:<br />/*<br />***********************************************************************************************************<br />**函数原型  :   UINT32 CANSendData(eCANNUM CanNum,UINT32 C

satanwxd的专栏 1万+

[转载]自己动手编写嵌入式Bootloader之(2

第二部分:通过网口下载内核映像要实现通过网口下载文件的功能,从底层到上层需要做的工作包括:开发板上的网卡芯片的驱动程序;TCP/IP协议栈的实现;TFTP客户端应用程序的实现。我们使用的OK2440开发板配备CS8900A网卡芯片。 为了简单起见,网络数据包的发送和接收都使用轮询方式,不使用中断;协议栈只使用ARP/IP/UDP协议,不涉及TCP及其他协议;应用程序

DerekYao的专栏 1216

嵌入式MCU BootLoader开发配置详细笔记教程

对于接触过嵌入式Linux系统开发的开发者们,想必对BootLoader是不陌生的,因为定制化移植Linux系统,最先接触的就是BootLoader程序。但如果是从单片机MCU起步的开发者,可能对BootLoader就不是那么熟悉了,因为单片机开发最先接触的往往是GPIO外设的驱动开发。但不管是嵌入式Linux的MPU开发,还是嵌入式单片机的MCU开发,BootLoader的功能都是一样的。它是嵌入式系统中一种特殊的软件程序,它在系统加电或复位后最先执行,负责初始化硬件设备、设置系统环境,并最终引导加载操作

weixin_49337111的博客 1万+

STM32U575RIT6(USART)学习

至少要有两个端,发送端和接收端信息的传递数据的交互。

qq_63858767的博客 394

泛联新安亮相(CIAC 2026)中国智能自动化大会,为机器人行业提供高可信代码保障

泛联新安亮相2026中国智能自动化大会,聚焦机器人主题,展示覆盖“研发-测试-安全”全链路的可信方案,包括智能开发体系、天神系列安全工具及质量测试链,助力机器人底层代码高可信、安全可控。

flxa19310090839的博客 184

STM32常用库(标准库与 HAL 库)对比和常用函数全解

标准库是"外设指针 + 配置结构体"两个参数,HAL 是"一个句柄"搞定一切。前者更贴近寄存器、更透明;后者更面向对象、更易移植。

2401_89054280的博客 491

STM32 学习笔记:GPIO 输入与 C 语言基础

typedef#define0/* ... */好处是可读、集中修改;风险是它本质上是文本替换,不会像真正的类型那样进行完整的类型检查。带参数的宏还必须注意括号和副作用。

qq_45821285的博客 265

【stm32】I2C 通信协议

I2C 是一种串行、同步、半双工的通信协议,只用两根线(SCL 时钟线 + SDA 数据线)就能在集成电路之间进行数据的双向传输,典型用于单片机与传感器、存储器等低速外围设备的连接。串行:数据一位一位地传,不像并行口那样 8 位一起传。同步:有专门的时钟线(SCL)来"打节拍",收、发双方跟着节拍走,所以不需要像串口那样约定波特率。半双工:同一时刻只能一个方向传数据——要么主发从收,要么从发主收,不能同时双向。Q:请讲讲 I2C 是什么?为什么会有 I2C 这个东西?

zhangzhiqi1100的博客 284

每日开源硬件精选简报 2026-08-16

IPD100 是一款 10 Hz–1 MHz(10 Hz 步进)手持数字电桥(LCR 表),采用 STM32H7 + FPGA 双核架构,测量范围 10 mΩ–10 MΩ,配备丝滑的触摸屏 UI,支持自定义点数扫频功能;TYPE-C 接口充电,待机约四小时,户外可连续使用半天。整机由主体(3 块 PCB 层叠)、开尔文测试线与外壳三部分组成。

飞多学堂的博客 80

【VS Code插件开发】通用功能(二)

本篇主要从command(命令)、menus(菜单)、快捷键三个方面讲解,都归于通用功能。

编码世界 1万+

2026年最新整理120余份智慧交通+交通行业AI大模型应用赋能+智慧高速+公共交通行业应用方案合集(PPT+WORD格式).rar

智慧交通是指利用先进的信息技术、数据通信传输技术、电子传感技术、控制技术及计算机技术等对交通系统进行实时、高效、精确的管控与服务,以实现交通管理智能化、运输服务高效化和交通运营的安全、畅通、便捷和环保。随着人工智能技术的快速发展,AI大模型在智慧交通领域扮演着越来越重要的角色,这些模型能够处理和分析海量交通数据,为交通规划、交通流量控制、事故预防与处理、出行服务等方面提供科学的决策支持。智慧高速是智慧交通的重要组成部分,它运用现代信息技术,通过自动化、信息化手段,实现对高速公路交通流的精确控制和管理,提高道路安全水平和通行效率,减少拥堵和事故,从而为驾驶者提供更加安全、舒适、高效的驾驶体验。智慧高速的实现依托于精确的道路传感器、视频监控设备、通讯网络以及先进的数据处理中心。公共交通作为城市交通的重要组成部分,其智能化水平的提升能够显著增强公共交通系统的吸引力,改善市民出行体验,提高公共交通的效率与便捷性。智慧公共交通系统通常包括智能化的车辆调度系统、实时的乘客信息服务、高效的票务管理系统以及安全的车辆运行控制系统等。这些系统通过集成多种技术,能够为乘客和管理者提供全面、准确的交通信息,提升公共交通的整体服务水平。在这些技术应用中,AI大模型的赋能作用不可忽视。例如,在智能交通管理中,通过对交通数据的深度学习与分析,AI可以预测交通流量趋势,优化信号灯配时,从而有效缓解交通拥堵。在公共交通方面,AI模型能够分析大量公共交通运行数据,为车辆调度、线路优化提供决策支持,同时还能提升乘客服务质量,如通过智能机器人或者移动应用为乘客提供实时的换乘信息和交通指导。随着技术的不断进步,智慧交通系统的构建将更加依赖于大数据、云计算、物联网等技术的融合应用。通过建立完善的数据采集、处理和分析体系,可以实现对交通系统运行状态的实时监控和分析,为智能决策提供支持,推动智慧交通和智

上一篇: 自己动手编写嵌入式Bootloader之(1)
下一篇: 自己动手编写嵌入式Bootloader之(3)
jun2ran
博客等级 码龄18年 101粉丝 183原创
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值