简介:这份资源面向使用STM32F103进行国产DAC芯片开发的嵌入式工程师与学习者,针对SGM5348-12这颗8路输出、0-5V(10mA)的国产DAC芯片,提供了一套可直接参考的SPI应用驱动。由于网络上缺少该芯片的驱动示例,作者结合实际调试经验整理出这份代码,帮助读者快速完成芯片的初始化、通道配置与电压输出控制,降低上手门槛。压缩包共2个文件,包含1个c源文件与1个h头文件,分别承载SPI通信与寄存器配置逻辑以及对外接口声明,整体约3KB,结构精简,便于直接移植进现有工程。目前已有2498人学习下载,说明该驱动在国产DAC替代方案中具有一定参考价值。配合SGM5348-12数据手册阅读代码,读者可较快理解时序、命令字与通道映射关系,并在此基础上扩展多路输出、电压校准等应用,适合作为国产8路DAC驱动开发的入门与移植参考。
1. 国产DAC芯片SGM5348-12在STM32F103上的驱动落地:从选型到跑通
如果你手头有一块STM32F103的开发板,又恰好拿到几片SGM5348-12,想把它们连起来输出一个稳定的模拟电压,那这篇笔记就是为你写的。SGM5348-12是一颗12位、8通道、SPI接口的电压输出型DAC芯片,国产替代场景里出现频率越来越高,常见于工业控制、可编程电压源、传感器校准这类需要多通道模拟输出的场合。它和STM32F103搭配的核心逻辑并不复杂:F103用SPI把12位数据写进芯片的移位寄存器,芯片内部R-2R电阻网络把数字量转成电压,再通过输出缓冲器驱动负载。但真正动手时你会发现,时序模式选错、参考电压接法不对、通道地址算错,任何一个细节都能让你在示波器前盯着一条纹波发呆。下面按“先搞清楚芯片怎么用、再动手写驱动、最后排坑”的顺序展开,适合刚接触这颗芯片的嵌入式工程师,也适合想从STC或Arduino平台迁移到STM32的开发者。
2. SGM5348-12的通信协议与STM32F103的SPI匹配:先看懂时序再接线
2.1 芯片的帧格式与命令字结构
SGM5348-12的SPI接口不是标准的“发完16位就完事”那种。它的每一帧是24位,前8位是命令字节,后16位是数据。命令字节里包含了通道选择位和功能位。具体来说,命令字节的bit7~bit4是通道地址(0~7对应8个通道),bit3是保留位,bit2是掉电控制位,bit1和bit0是操作模式位。数据部分虽然是16位,但实际有效的只有低12位,高4位是无关位,通常填0。这意味着你不能直接用STM32的SPI_DataSize_16Bit模式发一次就完事,要么发三次8位,要么发一次8位加一次16位,或者干脆用软件模拟SPI。我一般会选硬件SPI发三次8位,因为F103的SPI支持8位数据帧,发三次刚好凑成24个时钟,逻辑清晰,中断里也好处理。
这里有个容易翻车的点:SGM5348-12的SPI模式是CPOL=0、CPHA=0,也就是空闲时SCK为低,数据在SCK上升沿被采样。STM32F103的SPI1配置成Master、全双工、8位、MSB First、分频系数根据你的SCK需求来定。如果你用SPI2或SPI3,配置逻辑一样,只是引脚不同。注意F103的SPI最高时钟是PCLK2的一半(SPI1)或PCLK1的一半(SPI2/3),72MHz主频下SPI1最高36MHz,但SGM5348-12的SCK最高只到30MHz,所以分频系数至少设成4,也就是18MHz,稳妥一点用8分频到9MHz,时序余量更大。
2.2 硬件连接与参考电压的接法
接线本身不复杂,但参考电压的接法直接决定输出精度。SGM5348-12的VREF引脚接外部基准,输出范围是0到VREF。如果你用3.3V供电,VREF也接3.3V,那输出满量程就是3.3V,但3.3V轨上的噪声会直接调制到输出上。我一般会外接一颗2.5V的基准芯片,比如REF5025或类似型号,这样输出范围是0到2.5V,噪声和温漂都小很多。VDD接3.3V,注意在VDD和VREF引脚旁边各放一个0.1uF的陶瓷电容到地,距离引脚越近越好。输出引脚OUT0~OUT7直接接负载,但如果负载是容性的,比如长电缆或某些传感器,最好串一个几十欧姆的电阻再并一个小电容,防止输出缓冲器振荡。
SPI引脚方面,STM32F103的SPI1默认是PA5(SCK)、PA6(MISO)、PA7(MOSI),片选CS用任意GPIO,比如PA4。SGM5348-12的SDI接MOSI,SCK接SCK,CS接GPIO,SDO可以不接,因为这颗芯片只写不读。如果你要读回寄存器状态,那得接SDO,但大多数场景下不需要。注意CS的时序:SGM5348-12要求CS在24个时钟周期内保持低电平,发完最后一个时钟后拉高。如果你用硬件SPI的NSS引脚自动控制,可能会因为NSS脉冲宽度不够导致帧错误,所以我习惯用普通GPIO手动控制CS,发数据前拉低,发完拉高,中间不要有额外延时。
2.3 用STM32CubeMX配置SPI的实操步骤
打开CubeMX,选好你的F103型号,在Connectivity里启用SPI1,Mode选Full-Duplex Master,Hardware NSS Signal选Disable。Parameter Settings里,Frame Format选Motorola,Data Size选8 Bits,First Bit选MSB First,Prescaler选8,Clock Polarity选Low,Clock Phase选1 Edge。这样配置出来的SPI就是CPOL=0、CPHA=0,SCK频率9MHz。GPIO方面,PA5和PA7自动配置为复用推挽,PA6可以不管,PA4手动配置为GPIO Output,初始电平高。生成代码后,在main.c里先调用HAL_SPI_Init,然后写一个发送24位的函数。
// 发送24位数据到SGM5348-12 // cmd: 命令字节,包含通道和模式 // data: 12位数据,右对齐 void SGM5348_Write(uint8_t cmd, uint16_t data) { uint8_t buf[3]; buf[0] = cmd; buf[1] = (data >> 8) & 0x0F; // 高4位有效 buf[2] = data & 0xFF; // 低8位 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // CS拉低 HAL_SPI_Transmit(&hspi1, buf, 3, 100); // 发3字节 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS拉高 }这段代码里,buf[1]只取data的高4位,因为SGM5348-12的12位数据在16位帧里是右对齐的,高4位无效。HAL_SPI_Transmit的第四个参数是超时时间,单位毫秒,100足够。注意CS拉低和拉高之间不要加延时,HAL_SPI_Transmit本身会阻塞到发完,发完立刻拉高CS即可。如果你在中断里调用这个函数,确保SPI没有其他任务在用,否则要加互斥锁。
3. 从零写一个可复用的SGM5348-12驱动:寄存器映射与通道更新
3.1 命令字节的位定义与通道地址计算
命令字节的bit7~bit4是通道地址,0x00对应通道0,0x10对应通道1,以此类推,0x70对应通道7。bit3保留,写0。bit2是掉电控制,写0是正常工作,写1是掉电模式。bit1和bit0是操作模式:00是写数据到输入寄存器但不更新输出,01是写数据并更新输出,10是写数据到输入寄存器并更新所有通道,11是保留。实际使用中,最常用的是01模式,也就是写一个通道就立刻更新那个通道的输出。如果你要同时更新多个通道,可以先用00模式把数据写到各通道的输入寄存器,最后发一个命令字节0x0F(通道地址0,模式10)来同时更新所有通道。这个特性在做多通道同步输出时很有用,比如三相电压源需要同时改变三个通道的输出。
通道地址的计算很简单:channel << 4。比如channel=3,地址就是0x30。命令字节 = (channel << 4) | (mode << 0)。mode=1就是写并更新。所以写通道3输出2.0V(假设VREF=2.5V)的命令字节是0x31,数据是(2.0/2.5)*4096 = 3277,十六进制0xCCD。注意数据是12位,最大值4095对应VREF,所以2.0V对应3276.8,取3277。
3.2 用结构体封装驱动并支持多片级联
如果你的系统里不止一片SGM5348-12,比如需要16个通道,那就得级联。SGM5348-12支持菊花链,把第一片的SDO接第二片的SDI,CS共用,SCK共用。发数据时,先发第二片的24位,再发第一片的24位,这样第一片的数据会被推到第二片。STM32这边只需要发48位,也就是6个字节。为了代码好维护,我一般用一个结构体来管理每片DAC的CS引脚和当前通道值。
typedef struct { GPIO_TypeDef *cs_port; uint16_t cs_pin; uint16_t channel_val[8]; // 缓存各通道当前值 } SGM5348_Handle; // 写单通道并更新 void SGM5348_SetChannel(SGM5348_Handle *h, uint8_t ch, uint16_t val) { uint8_t cmd = (ch << 4) | 0x01; // 写并更新 uint8_t buf[3]; buf[0] = cmd; buf[1] = (val >> 8) & 0x0F; buf[2] = val & 0xFF; HAL_GPIO_WritePin(h->cs_port, h->cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, buf, 3, 100); HAL_GPIO_WritePin(h->cs_port, h->cs_pin, GPIO_PIN_SET); h->channel_val[ch] = val; }这个结构体里,channel_val数组用来缓存每个通道的当前值,方便做增量调整或掉电恢复。如果你要级联两片,就定义两个SGM5348_Handle,CS引脚不同,但SPI总线共用。发数据时先发第二片的3字节,再发第一片的3字节,CS在两次发送之间保持低电平。注意HAL_SPI_Transmit一次发6字节也行,但要把两片的buf拼在一起。
3.3 电压换算与校准:从数字量到实际输出
电压换算公式是:Vout = (val / 4096) * VREF。但实际芯片有增益误差和偏移误差,尤其是国产芯片批次之间可能有差异。如果你需要高精度,建议做两点校准:先写0,测实际输出V0;再写4095,测实际输出VFS。然后实际写值 = (目标电压 - V0) / (VFS - V0) * 4095。这个校准系数可以存在STM32的Flash里,上电时读出来。我一般会在驱动里加一个校准结构体,存gain和offset,换算时先算理论值再套校准。
typedef struct { float gain; float offset; } DAC_Calib; uint16_t DAC_VoltageToCode(float voltage, float vref, DAC_Calib *cal) { float code = (voltage / vref) * 4095.0f; code = code * cal->gain + cal->offset; if (code < 0) code = 0; if (code > 4095) code = 4095; return (uint16_t)(code + 0.5f); }这段代码里,gain和offset默认是1.0和0.0,校准后填入实际值。注意浮点运算在F103上没有硬件FPU,但DAC更新频率不高的话,软件浮点完全够用。如果你在中断里频繁调用,建议改成定点运算,把gain和offset放大10000倍用整数算。
4. 避坑与排查:SGM5348-12驱动中最容易翻车的5个点
4.1 输出一直是0或者满量程
现象:写任何值,输出要么是0V,要么是VREF,不随数据变化。原因通常是SPI模式不对,数据没被正确移入。SGM5348-12要求CPOL=0、CPHA=0,如果你配成了CPHA=1,数据在第二个边沿采样,就会错位。解决:用示波器看SCK和MOSI,确认在SCK上升沿时MOSI稳定。另外检查CS是否在发数据前拉低、发完后拉高,如果CS一直低,芯片可能把多个帧当成一个长帧,导致数据错位。
4.2 输出有规律纹波或跳动
现象:输出直流电压上叠加了几十毫伏的纹波,频率和SPI时钟或系统时钟相关。原因一般是VREF引脚没有足够去耦,或者VDD和VREF共用同一个3.3V且没有隔离。解决:VREF单独接基准芯片,VDD和VREF各加0.1uF和10uF电容,布局时电容尽量靠近引脚。如果纹波频率是50Hz或100Hz,那可能是电源纹波,检查你的3.3V LDO是否足够干净。
4.3 多通道更新时通道错位
现象:想更新通道3,结果通道2变了。原因通常是命令字节的通道地址算错,比如把channel直接当命令字节发了,没有左移4位。解决:确认命令字节 = (channel << 4) | mode。另外如果你用了级联,注意数据发送顺序,先发的数据会被推到后面的芯片,所以先发最后一片的数据。
4.4 SPI发送阻塞导致系统卡顿
现象:调用SGM5348_Write后,系统响应变慢,或者中断被延迟。原因:HAL_SPI_Transmit是阻塞式发送,9MHz下发3字节大概2.7微秒,但如果SPI被其他任务占用,或者超时时间设得太长,就会阻塞。解决:如果DAC更新不频繁,阻塞没问题;如果频繁更新,改用HAL_SPI_Transmit_IT或DMA,把CS控制放到发送完成回调里。注意用中断方式时,CS拉高要在回调里做,否则会在数据发完前拉高。
4.5 上电瞬间输出尖峰
现象:系统上电时,DAC输出端出现一个短时尖峰,可能损坏后级电路。原因:SGM5348-12上电时输出寄存器默认是0,但输出缓冲器使能瞬间可能有毛刺。解决:在输出端加一个RC滤波,或者用一颗模拟开关在上电稳定后再接通负载。另外可以在初始化时先写0到所有通道,再写目标值,避免上电默认值不确定。
5. 进阶技巧:用DMA和定时器做多通道同步输出与波形生成
如果你要做的不只是设置静态电压,而是生成波形,比如正弦波、三角波,那SGM5348-12的8个通道可以派上大用场。一个典型的场景是:用STM32F103的定时器触发DMA,DMA把预先算好的波形数据表依次搬到SPI数据寄存器,SPI自动发到DAC,这样CPU几乎不参与,输出频率可以做到几十kHz。具体做法是:先把一个周期的波形采样成N个点,每个点12位,存到数组里。然后配置TIM2或TIM3,设置更新频率 = 采样率。DMA配置成SPI_TX模式,源地址是波形数组,目标地址是SPI1->DR,传输长度是N,循环模式。每次定时器更新事件触发一次DMA请求,DMA搬一个16位数据到SPI,但SPI是8位帧,所以需要把12位数据拆成两个8位,或者用SPI的16位数据帧模式。F103的SPI支持16位数据帧,但SGM5348-12需要24位帧,所以16位模式不够。我一般用DMA搬3个字节到SPI,但DMA的传输单位是字节,所以波形数组要存成3字节一组,每组对应一个采样点。这样DMA每次搬3个字节,SPI发3个字节,CS在DMA传输期间保持低电平,传输完成后拉高。CS的控制可以用定时器的另一个通道或者DMA传输完成中断来做。
// 波形数组,每个点3字节:命令+数据高4位+数据低8位 // 假设只更新通道0,命令字节0x01 uint8_t wave_table[WAVE_LEN * 3]; void Wave_Init(void) { for (int i = 0; i < WAVE_LEN; i++) { float v = 1.25f + 1.25f * sinf(2 * 3.14159f * i / WAVE_LEN); // 0~2.5V正弦 uint16_t code = (uint16_t)(v / 2.5f * 4095); wave_table[i*3] = 0x01; // 通道0,写并更新 wave_table[i*3+1] = (code >> 8) & 0x0F; wave_table[i*3+2] = code & 0xFF; } // 配置DMA和定时器,此处省略具体寄存器配置 }这段代码生成一个正弦波表,每个点3字节。DMA配置成循环模式,定时器触发频率决定输出频率。注意SPI的发送速率要大于定时器触发频率乘以24,否则DMA会溢出。比如定时器触发100kHz,SPI时钟9MHz,发24位需要2.67微秒,100kHz周期是10微秒,余量足够。如果要做多通道同步,可以在波形表里交替写不同通道的命令字节,DMA会依次发出,实现多通道时分复用输出。这个技巧在做可编程三相电源或LED调光时很实用,但要注意DAC的建立时间,SGM5348-12的建立时间典型值是6微秒,所以输出频率不要超过几十kHz,否则波形会失真。
我自己的习惯是:先用静态电压把驱动调通,再用示波器看阶跃响应,确认建立时间符合预期,最后才上DMA波形。每次改SPI分频或DMA配置,都要重新测一遍输出,因为时序余量可能被吃掉。希望帮到你。
本文还有配套的精品资源,点击获取