I wanted something more minimal for programming a CH32V003 so I had GPT 5.4 port https://github.com/cnlohr/ch32fun/tree/master/minichlink to pure Golang.
It works for flashing the minimal blink program at least and does it in 500ms.
I wanted something more minimal for programming a CH32V003 so I had GPT 5.4 port https://github.com/cnlohr/ch32fun/tree/master/minichlink to pure Golang.
It works for flashing the minimal blink program at least and does it in 500ms.
| package main | |
| import ( | |
| "encoding/binary" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "strconv" | |
| "strings" | |
| "syscall" | |
| "time" | |
| "unsafe" | |
| ) | |
| const ( | |
| commandTimeoutMS = 200 | |
| writeTimeoutMS = 5000 | |
| asyncChunkSize = 64 | |
| asyncWindow = 16 | |
| chipCH32V003 = 0x09 | |
| sectorSize = 64 | |
| flashAddr = 0x08000000 | |
| dmdata0 = 0x04 | |
| dmdata1 = 0x05 | |
| dmcontrol = 0x10 | |
| dmstatus = 0x11 | |
| dmhartinfo = 0x12 | |
| dmabstractcs = 0x16 | |
| dmcommand = 0x17 | |
| dmabstractauto = 0x18 | |
| dmprogbuf0 = 0x20 | |
| dmprogbuf1 = 0x21 | |
| dmprogbuf2 = 0x22 | |
| dmprogbuf3 = 0x23 | |
| dmprogbuf4 = 0x24 | |
| dmprogbuf5 = 0x25 | |
| dmchipid = 0x7f | |
| flashSTATR = 0x4002200c | |
| flashCTLR = 0x40022010 | |
| flashADDR = 0x40022014 | |
| flashKEYR = 0x40022004 | |
| flashOBKEYR = 0x40022008 | |
| flashMODEKEYR = 0x40022024 | |
| flashKey1 = 0x45670123 | |
| flashKey2 = 0xCDEF89AB | |
| crStartSet = 0x00000040 | |
| crPagePG = 0x00010000 | |
| crPageER = 0x00020000 | |
| crBufLoad = 0x00040000 | |
| crBufRST = 0x00080000 | |
| flashStatrWrErr = 0x00000010 | |
| stateVoid = iota | |
| stateWriteSeq | |
| stateReadSeq | |
| wchVendorID = "1a86" | |
| wchProductID = "8010" | |
| usbInterface = 0 | |
| iocNRBits = 8 | |
| iocTypeBits = 8 | |
| iocSizeBits = 14 | |
| iocDirBits = 2 | |
| iocNRShift = 0 | |
| iocTypeShift = iocNRShift + iocNRBits | |
| iocSizeShift = iocTypeShift + iocTypeBits | |
| iocDirShift = iocSizeShift + iocSizeBits | |
| iocWrite = 1 | |
| iocRead = 2 | |
| usbdevfsUrbTypeBulk = 3 | |
| usbdevfsUrbBulkContinuation = 0x04 | |
| ) | |
| type linkE struct { | |
| file *os.File | |
| path string | |
| stateTag int | |
| currentAddr uint32 | |
| lastWriteFlag bool | |
| autoIncrement bool | |
| flashUnlocked bool | |
| } | |
| type usbdevfsBulktransfer struct { | |
| Ep uint32 | |
| Len uint32 | |
| Timeout uint32 | |
| Data uintptr | |
| } | |
| type usbdevfsDisconnectClaim struct { | |
| Interface uint32 | |
| Flags uint32 | |
| Driver [256]byte | |
| } | |
| type usbdevfsUrb struct { | |
| Type uint8 | |
| Endpoint uint8 | |
| Status int32 | |
| Flags uint32 | |
| Buffer uintptr | |
| BufferLength int32 | |
| ActualLength int32 | |
| StartFrame int32 | |
| NumberOfPackets int32 | |
| ErrorCount int32 | |
| Signr uint32 | |
| UserContext uintptr | |
| } | |
| type inflightURB struct { | |
| urb *usbdevfsUrb | |
| data []byte | |
| } | |
| func main() { | |
| imagePath := flag.String("file", "examples/blink/blink.bin", "firmware binary to flash") | |
| address := flag.Uint("addr", flashAddr, "flash address") | |
| flag.Parse() | |
| if err := run(*imagePath, uint32(*address)); err != nil { | |
| fmt.Fprintln(os.Stderr, err) | |
| os.Exit(1) | |
| } | |
| } | |
| func run(imagePath string, address uint32) error { | |
| image, err := os.ReadFile(imagePath) | |
| if err != nil { | |
| return fmt.Errorf("read image: %w", err) | |
| } | |
| if len(image) == 0 { | |
| return errors.New("image is empty") | |
| } | |
| link, err := openLink() | |
| if err != nil { | |
| return err | |
| } | |
| defer link.close() | |
| if err := link.setupProgrammer(); err != nil { | |
| return err | |
| } | |
| chipID, err := link.identifyChip() | |
| if err != nil { | |
| return err | |
| } | |
| fmt.Printf("Detected CH32V003\n") | |
| fmt.Printf("DMCHIPID: %08x\n", chipID) | |
| fmt.Printf("Flashing %s (%d bytes)\n", imagePath, len(image)) | |
| if err := link.flashImage(address, image); err != nil { | |
| return err | |
| } | |
| if err := link.rebootTarget(); err != nil { | |
| return err | |
| } | |
| fmt.Println("Image written.") | |
| return nil | |
| } | |
| func openLink() (*linkE, error) { | |
| devicePath, err := findWCHLinkDevice() | |
| if err != nil { | |
| return nil, err | |
| } | |
| f, err := os.OpenFile(devicePath, os.O_RDWR, 0) | |
| if err != nil { | |
| return nil, fmt.Errorf("open %s: %w", devicePath, err) | |
| } | |
| l := &linkE{file: f, path: devicePath, stateTag: stateVoid} | |
| if err := l.claimInterface(usbInterface); err != nil { | |
| f.Close() | |
| return nil, err | |
| } | |
| _ = l.drain() | |
| return l, nil | |
| } | |
| func findWCHLinkDevice() (string, error) { | |
| matches, err := filepath.Glob("/sys/bus/usb/devices/*") | |
| if err != nil { | |
| return "", fmt.Errorf("scan usb sysfs: %w", err) | |
| } | |
| for _, dev := range matches { | |
| vendor, err := os.ReadFile(filepath.Join(dev, "idVendor")) | |
| if err != nil { | |
| continue | |
| } | |
| product, err := os.ReadFile(filepath.Join(dev, "idProduct")) | |
| if err != nil { | |
| continue | |
| } | |
| if strings.TrimSpace(string(vendor)) != wchVendorID || strings.TrimSpace(string(product)) != wchProductID { | |
| continue | |
| } | |
| busnum, err := readIntFile(filepath.Join(dev, "busnum")) | |
| if err != nil { | |
| continue | |
| } | |
| devnum, err := readIntFile(filepath.Join(dev, "devnum")) | |
| if err != nil { | |
| continue | |
| } | |
| return fmt.Sprintf("/dev/bus/usb/%03d/%03d", busnum, devnum), nil | |
| } | |
| return "", errors.New("could not find WCH-LinkE in RISC-V mode (VID:PID 1a86:8010)") | |
| } | |
| func readIntFile(path string) (int, error) { | |
| raw, err := os.ReadFile(path) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return strconv.Atoi(strings.TrimSpace(string(raw))) | |
| } | |
| func (l *linkE) claimInterface(iface uint32) error { | |
| if err := ioctlValuePtr(l.file.Fd(), usbdevfsClaimInterface(), iface); err == nil { | |
| return nil | |
| } else if !errors.Is(err, syscall.EBUSY) { | |
| return fmt.Errorf("claim interface %d on %s: %w", iface, l.path, err) | |
| } | |
| claim := usbdevfsDisconnectClaim{Interface: iface} | |
| if err := ioctlPtr(l.file.Fd(), usbdevfsDisconnectClaimReq(), unsafe.Pointer(&claim)); err != nil { | |
| return fmt.Errorf("disconnect+claim interface %d on %s: %w", iface, l.path, err) | |
| } | |
| return nil | |
| } | |
| func (l *linkE) close() { | |
| if l.file != nil { | |
| _ = l.file.Close() | |
| } | |
| } | |
| func (l *linkE) drain() error { | |
| buf := make([]byte, 1024) | |
| err := l.bulkIn(0x81, buf, 1) | |
| if errors.Is(err, syscall.ETIMEDOUT) { | |
| return nil | |
| } | |
| return err | |
| } | |
| func ioctlRequest(dir, typ, nr, size uintptr) uintptr { | |
| return (dir << iocDirShift) | (typ << iocTypeShift) | (nr << iocNRShift) | (size << iocSizeShift) | |
| } | |
| func usbdevfsBulkReq() uintptr { | |
| return ioctlRequest(iocRead|iocWrite, 'U', 2, unsafe.Sizeof(usbdevfsBulktransfer{})) | |
| } | |
| func usbdevfsClaimInterface() uintptr { | |
| return ioctlRequest(iocRead, 'U', 15, unsafe.Sizeof(uint32(0))) | |
| } | |
| func usbdevfsDisconnectClaimReq() uintptr { | |
| return ioctlRequest(iocRead, 'U', 27, unsafe.Sizeof(usbdevfsDisconnectClaim{})) | |
| } | |
| func usbdevfsSubmitURB() uintptr { | |
| return ioctlRequest(iocRead, 'U', 10, unsafe.Sizeof(usbdevfsUrb{})) | |
| } | |
| func usbdevfsReapURB() uintptr { | |
| return ioctlRequest(iocWrite, 'U', 12, unsafe.Sizeof(uintptr(0))) | |
| } | |
| func usbdevfsReapURBNDelay() uintptr { | |
| return ioctlRequest(iocWrite, 'U', 13, unsafe.Sizeof(uintptr(0))) | |
| } | |
| func ioctlNoArg(fd uintptr, req uintptr, value uintptr) error { | |
| _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, value) | |
| if errno != 0 { | |
| return errno | |
| } | |
| return nil | |
| } | |
| func ioctlValuePtr(fd uintptr, req uintptr, value uint32) error { | |
| v := value | |
| _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, uintptr(unsafe.Pointer(&v))) | |
| if errno != 0 { | |
| return errno | |
| } | |
| return nil | |
| } | |
| func ioctlPtr(fd uintptr, req uintptr, ptr unsafe.Pointer) error { | |
| _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, uintptr(ptr)) | |
| if errno != 0 { | |
| return errno | |
| } | |
| return nil | |
| } | |
| func bulkTransfer(fd uintptr, ep uint32, data []byte, timeoutMS int) (int, error) { | |
| var ptr uintptr | |
| if len(data) > 0 { | |
| ptr = uintptr(unsafe.Pointer(&data[0])) | |
| } | |
| req := usbdevfsBulktransfer{ | |
| Ep: ep, | |
| Len: uint32(len(data)), | |
| Timeout: uint32(timeoutMS), | |
| Data: ptr, | |
| } | |
| r1, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, usbdevfsBulkReq(), uintptr(unsafe.Pointer(&req))) | |
| if errno != 0 { | |
| return 0, errno | |
| } | |
| n := int(r1) | |
| if ep&0x80 == 0 && n == 0 && len(data) > 0 { | |
| n = len(data) | |
| } | |
| return n, nil | |
| } | |
| func submitBulkURB(fd uintptr, ep uint8, data []byte, flags uint32) (*usbdevfsUrb, error) { | |
| if len(data) == 0 { | |
| return nil, nil | |
| } | |
| urb := &usbdevfsUrb{ | |
| Type: usbdevfsUrbTypeBulk, | |
| Endpoint: ep, | |
| Flags: flags, | |
| Buffer: uintptr(unsafe.Pointer(&data[0])), | |
| BufferLength: int32(len(data)), | |
| } | |
| if err := ioctlPtr(fd, usbdevfsSubmitURB(), unsafe.Pointer(urb)); err != nil { | |
| return nil, err | |
| } | |
| return urb, nil | |
| } | |
| func reapURB(fd uintptr, req uintptr) (*usbdevfsUrb, error) { | |
| var reap uintptr | |
| _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, uintptr(unsafe.Pointer(&reap))) | |
| if errno != 0 { | |
| return nil, errno | |
| } | |
| return (*usbdevfsUrb)(unsafe.Pointer(reap)), nil | |
| } | |
| func removeInflight(inflight []inflightURB, urb *usbdevfsUrb) (inflightURB, []inflightURB, error) { | |
| for i, item := range inflight { | |
| if item.urb == urb { | |
| last := inflight[len(inflight)-1] | |
| inflight[i] = last | |
| inflight = inflight[:len(inflight)-1] | |
| return item, inflight, nil | |
| } | |
| } | |
| return inflightURB{}, inflight, fmt.Errorf("reaped unexpected urb %p", urb) | |
| } | |
| func checkCompleted(item inflightURB) error { | |
| if item.urb.Status != 0 { | |
| return syscall.Errno(-item.urb.Status) | |
| } | |
| if int(item.urb.ActualLength) != len(item.data) { | |
| return fmt.Errorf("short async write: %d/%d", item.urb.ActualLength, len(item.data)) | |
| } | |
| return nil | |
| } | |
| func submitBulkPipeline(fd uintptr, ep uint8, data []byte) error { | |
| if len(data) == 0 { | |
| return nil | |
| } | |
| inflight := make([]inflightURB, 0, asyncWindow) | |
| for off := 0; off < len(data); off += asyncChunkSize { | |
| end := off + asyncChunkSize | |
| if end > len(data) { | |
| end = len(data) | |
| } | |
| chunk := data[off:end] | |
| flags := uint32(0) | |
| if off > 0 { | |
| flags |= usbdevfsUrbBulkContinuation | |
| } | |
| urb, err := submitBulkURB(fd, ep, chunk, flags) | |
| if err != nil { | |
| return err | |
| } | |
| inflight = append(inflight, inflightURB{urb: urb, data: chunk}) | |
| for { | |
| reaped, err := reapURB(fd, usbdevfsReapURBNDelay()) | |
| if err != nil { | |
| if errors.Is(err, syscall.EAGAIN) { | |
| break | |
| } | |
| return err | |
| } | |
| item, next, err := removeInflight(inflight, reaped) | |
| inflight = next | |
| if err != nil { | |
| return err | |
| } | |
| if err := checkCompleted(item); err != nil { | |
| return err | |
| } | |
| } | |
| for len(inflight) >= asyncWindow { | |
| progress := false | |
| for { | |
| reaped, err := reapURB(fd, usbdevfsReapURBNDelay()) | |
| if err != nil { | |
| if errors.Is(err, syscall.EAGAIN) { | |
| break | |
| } | |
| return err | |
| } | |
| progress = true | |
| item, next, err := removeInflight(inflight, reaped) | |
| inflight = next | |
| if err != nil { | |
| return err | |
| } | |
| if err := checkCompleted(item); err != nil { | |
| return err | |
| } | |
| } | |
| if !progress { | |
| time.Sleep(100 * time.Microsecond) | |
| } | |
| } | |
| } | |
| for len(inflight) > 0 { | |
| reaped, err := reapURB(fd, usbdevfsReapURBNDelay()) | |
| if err != nil { | |
| if errors.Is(err, syscall.EAGAIN) { | |
| time.Sleep(100 * time.Microsecond) | |
| continue | |
| } | |
| return err | |
| } | |
| item, next, err := removeInflight(inflight, reaped) | |
| inflight = next | |
| if err != nil { | |
| return err | |
| } | |
| if err := checkCompleted(item); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func (l *linkE) bulkOut(ep uint8, data []byte) error { | |
| if len(data) == 0 { | |
| return nil | |
| } | |
| if len(data) > 64 { | |
| if err := submitBulkPipeline(l.file.Fd(), ep, data); err != nil { | |
| return fmt.Errorf("usb async bulk out on %s: %w", l.path, err) | |
| } | |
| return nil | |
| } | |
| n, err := bulkTransfer(l.file.Fd(), uint32(ep), data, writeTimeoutMS) | |
| if err != nil { | |
| return fmt.Errorf("usb bulk out on %s: %w", l.path, err) | |
| } | |
| if n != len(data) { | |
| return fmt.Errorf("short write: %d/%d", n, len(data)) | |
| } | |
| return nil | |
| } | |
| func (l *linkE) bulkIn(ep uint8, buf []byte, timeoutMS int) error { | |
| _, err := l.bulkInCount(ep, buf, timeoutMS) | |
| return err | |
| } | |
| func (l *linkE) bulkInCount(ep uint8, buf []byte, timeoutMS int) (int, error) { | |
| if len(buf) == 0 { | |
| return 0, nil | |
| } | |
| n, err := bulkTransfer(l.file.Fd(), uint32(ep), buf, timeoutMS) | |
| if err != nil { | |
| return 0, fmt.Errorf("usb bulk in on %s: %w", l.path, err) | |
| } | |
| return n, nil | |
| } | |
| func (l *linkE) command(cmd []byte, replyLen int) ([]byte, error) { | |
| return l.commandWithTimeout(cmd, replyLen, commandTimeoutMS) | |
| } | |
| func (l *linkE) commandWithTimeout(cmd []byte, replyLen int, timeoutMS int) ([]byte, error) { | |
| if err := l.bulkOut(0x01, cmd); err != nil { | |
| return nil, fmt.Errorf("send %s: %w", hexBytes(cmd), err) | |
| } | |
| if replyLen <= 0 { | |
| replyLen = 4 | |
| } | |
| buf := make([]byte, 1024) | |
| n, err := l.bulkInCount(0x81, buf, timeoutMS) | |
| if err != nil { | |
| return nil, fmt.Errorf("recv for %s: %w", hexBytes(cmd), err) | |
| } | |
| return buf[:n], nil | |
| } | |
| func (l *linkE) setupProgrammer() error { | |
| _, _ = l.command([]byte{0x81, 0x0d, 0x01, 0xff}, 4) | |
| reply, err := l.command([]byte{0x81, 0x0d, 0x01, 0x01}, 7) | |
| if err != nil { | |
| return fmt.Errorf("read programmer version: %w", err) | |
| } | |
| if len(reply) < 6 { | |
| return fmt.Errorf("short programmer version reply: %s", hexBytes(reply)) | |
| } | |
| fmt.Printf("WCH Programmer is %s version %d.%d\n", programmerName(reply[5]), reply[3], reply[4]) | |
| if _, err := l.command([]byte{0x81, 0x0c, 0x02, 0x01, 0x02}, 4); err != nil { | |
| return fmt.Errorf("set default interface speed: %w", err) | |
| } | |
| var last []byte | |
| alreadyConnected := false | |
| for tries := 0; tries < 6; tries++ { | |
| reply, err = l.command([]byte{0x81, 0x0d, 0x01, 0x02}, 9) | |
| if err != nil { | |
| return fmt.Errorf("connect target: %w", err) | |
| } | |
| last = reply | |
| if len(reply) == 9 && (reply[4] == 0x00 || (reply[8] != 0x02 && reply[8] != 0x03 && reply[8] != 0x00)) { | |
| if alreadyConnected { | |
| break | |
| } | |
| alreadyConnected = true | |
| } | |
| if len(reply) != 4 && !(len(reply) >= 3 && reply[0] == 0x81 && reply[1] == 0x55 && reply[2] == 0x01) { | |
| break | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0x13}, 4); err != nil { | |
| return fmt.Errorf("force reset low: %w", err) | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0xff}, 4); err != nil { | |
| return fmt.Errorf("exit programming while retrying: %w", err) | |
| } | |
| time.Sleep(5 * time.Millisecond) | |
| if tries > 3 { | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0x03}, 4); err != nil { | |
| return fmt.Errorf("retry reset sequence: %w", err) | |
| } | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0b, 0x01, 0x01}, 4); err != nil { | |
| return fmt.Errorf("release reset line: %w", err) | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0x02}, 9); err != nil { | |
| return fmt.Errorf("retry target connect: %w", err) | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0xff}, 4); err != nil { | |
| return fmt.Errorf("retry exit programming: %w", err) | |
| } | |
| } | |
| if len(last) == 4 || (len(last) >= 3 && last[0] == 0x81 && last[1] == 0x55 && last[2] == 0x01) { | |
| return fmt.Errorf("link error, nothing connected to linker: %s", hexBytes(last)) | |
| } | |
| return nil | |
| } | |
| func (l *linkE) identifyChip() (uint32, error) { | |
| if _, err := l.readReg32(dmhartinfo); err != nil { | |
| return 0, fmt.Errorf("read DMHARTINFO: %w", err) | |
| } | |
| if err := l.writeReg32(dmcontrol, 0x80000001); err != nil { | |
| return 0, fmt.Errorf("init DMCONTROL: %w", err) | |
| } | |
| if err := l.writeReg32(dmcontrol, 0x80000001); err != nil { | |
| return 0, fmt.Errorf("re-halt target: %w", err) | |
| } | |
| chipID, err := l.readReg32(dmchipid) | |
| if err != nil { | |
| return 0, fmt.Errorf("read DMCHIPID: %w", err) | |
| } | |
| if chipID&0xfff00f00 != 0x00300500 { | |
| return 0, fmt.Errorf("unsupported chip id %08x, this tool currently only supports CH32V003", chipID) | |
| } | |
| if err := l.writeReg32(dmcontrol, 0x80000003); err != nil { | |
| return 0, fmt.Errorf("reset target: %w", err) | |
| } | |
| if err := l.writeReg32(dmcontrol, 0x80000001); err != nil { | |
| return 0, fmt.Errorf("halt after reset: %w", err) | |
| } | |
| if err := l.writeReg32(dmcontrol, 0x80000001); err != nil { | |
| return 0, fmt.Errorf("re-halt after reset: %w", err) | |
| } | |
| time.Sleep(10 * time.Millisecond) | |
| if _, err := l.command([]byte{0x81, 0x0c, 0x02, chipCH32V003, 0x01}, 4); err != nil { | |
| return 0, fmt.Errorf("set CH32V003 interface speed: %w", err) | |
| } | |
| return chipID, nil | |
| } | |
| func (l *linkE) flashImage(address uint32, image []byte) error { | |
| if err := l.voidHighLevelState(); err != nil { | |
| return err | |
| } | |
| if err := l.unlockFlash(); err != nil { | |
| return err | |
| } | |
| paddedLen := ((len(image) - 1) & ^(sectorSize - 1)) + sectorSize | |
| padded := make([]byte, paddedLen) | |
| for i := range padded { | |
| padded[i] = 0xff | |
| } | |
| copy(padded, image) | |
| for off := 0; off < len(padded); off += sectorSize { | |
| pageAddr := address + uint32(off) | |
| page := padded[off : off+sectorSize] | |
| if err := l.eraseSector(pageAddr); err != nil { | |
| return fmt.Errorf("erase sector at %08x: %w", pageAddr, err) | |
| } | |
| if err := l.writeFlashPage(pageAddr, page); err != nil { | |
| return fmt.Errorf("write page at %08x: %w", pageAddr, err) | |
| } | |
| } | |
| return nil | |
| } | |
| func (l *linkE) rebootTarget() error { | |
| if _, err := l.command([]byte{0x81, 0x0b, 0x01, 0x01}, 4); err != nil { | |
| return fmt.Errorf("release reset line: %w", err) | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0x02}, 9); err != nil { | |
| return fmt.Errorf("reconnect after flash: %w", err) | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0xff}, 4); err != nil { | |
| return fmt.Errorf("exit programming after flash: %w", err) | |
| } | |
| return nil | |
| } | |
| func (l *linkE) holdReset() error { | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0x02}, 9); err != nil { | |
| return fmt.Errorf("enter reset-hold mode: %w", err) | |
| } | |
| if _, err := l.command([]byte{0x81, 0x0d, 0x01, 0x01}, 4); err != nil { | |
| return fmt.Errorf("hold target in reset: %w", err) | |
| } | |
| return nil | |
| } | |
| func (l *linkE) voidHighLevelState() error { | |
| l.stateTag = stateVoid | |
| l.currentAddr = 0 | |
| l.lastWriteFlag = false | |
| l.autoIncrement = false | |
| return nil | |
| } | |
| func (l *linkE) waitForDoneOp(ignore bool) error { | |
| timeout := 100 | |
| for { | |
| rrv, err := l.readReg32(dmabstractcs) | |
| if err != nil { | |
| return err | |
| } | |
| if rrv&(1<<12) == 0 || timeout <= 0 { | |
| if ((rrv>>8)&7) != 0 || (rrv&(1<<12)) != 0 { | |
| if !ignore { | |
| ds, _ := l.readReg32(dmstatus) | |
| return fmt.Errorf("abstract command fault dmabstractcs=%08x dmstatus=%08x", rrv, ds) | |
| } | |
| if err := l.writeReg32(dmabstractcs, 0x00000700); err != nil { | |
| return err | |
| } | |
| return fmt.Errorf("abstract command fault dmabstractcs=%08x", rrv) | |
| } | |
| return nil | |
| } | |
| timeout-- | |
| } | |
| } | |
| func (l *linkE) readWordRaw(address uint32) (uint32, error) { | |
| steps := []struct { | |
| reg uint8 | |
| value uint32 | |
| }{ | |
| {dmabstractauto, 0}, | |
| {dmprogbuf0, 0x0004a403}, | |
| {dmprogbuf1, 0x00100073}, | |
| {dmdata0, address}, | |
| {dmcommand, 0x00231009}, | |
| {dmcommand, 0x00241000}, | |
| {dmcommand, 0x00221008}, | |
| } | |
| for _, step := range steps { | |
| if err := l.writeReg32(step.reg, step.value); err != nil { | |
| return 0, err | |
| } | |
| } | |
| return l.readReg32(dmdata0) | |
| } | |
| func (l *linkE) writeWordRaw(address, data uint32) error { | |
| steps := []struct { | |
| reg uint8 | |
| value uint32 | |
| }{ | |
| {dmabstractauto, 0}, | |
| {dmprogbuf0, 0x0084a023}, | |
| {dmprogbuf1, 0x00100073}, | |
| {dmdata0, address}, | |
| {dmcommand, 0x00231009}, | |
| {dmdata0, data}, | |
| {dmcommand, 0x00271008}, | |
| } | |
| for _, step := range steps { | |
| if err := l.writeReg32(step.reg, step.value); err != nil { | |
| return err | |
| } | |
| } | |
| return l.waitForDoneOp(false) | |
| } | |
| func (l *linkE) staticUpdatePROGBUFRegs() error { | |
| rr, err := l.readReg32(dmhartinfo) | |
| if err != nil { | |
| return fmt.Errorf("read hart info: %w", err) | |
| } | |
| data0Offset := 0xe0000000 | (rr & 0x7ff) | |
| steps := []struct { | |
| reg uint8 | |
| value uint32 | |
| }{ | |
| {dmabstractauto, 0}, | |
| {dmdata0, data0Offset}, | |
| {dmcommand, 0x0023100a}, | |
| {dmdata0, data0Offset + 4}, | |
| {dmcommand, 0x0023100b}, | |
| {dmdata0, flashSTATR}, | |
| {dmcommand, 0x0023100c}, | |
| {dmdata0, crPagePG | crBufLoad}, | |
| {dmcommand, 0x0023100d}, | |
| } | |
| for _, step := range steps { | |
| if err := l.writeReg32(step.reg, step.value); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func isFlashAddress(addr uint32) bool { | |
| return addr&0xe0000000 == 0 | |
| } | |
| func (l *linkE) writeWord(address, data uint32) error { | |
| isFlash := isFlashAddress(address) | |
| if l.stateTag != stateWriteSeq || l.lastWriteFlag != isFlash { | |
| didDisableReq := false | |
| if l.stateTag != stateWriteSeq { | |
| if err := l.writeReg32(dmabstractauto, 0); err != nil { | |
| return err | |
| } | |
| didDisableReq = true | |
| if l.stateTag != stateReadSeq { | |
| if err := l.staticUpdatePROGBUFRegs(); err != nil { | |
| return err | |
| } | |
| } | |
| if err := l.writeReg32(dmprogbuf0, 0x41844100); err != nil { | |
| return err | |
| } | |
| if err := l.writeReg32(dmprogbuf1, 0x0491c080); err != nil { | |
| return err | |
| } | |
| } | |
| if isFlash { | |
| if err := l.writeReg32(dmprogbuf2, 0x0001c184); err != nil { | |
| return err | |
| } | |
| if err := l.writeReg32(dmprogbuf3, 0x4200c254); err != nil { | |
| return err | |
| } | |
| if err := l.writeReg32(dmprogbuf4, 0xfc758805); err != nil { | |
| return err | |
| } | |
| if err := l.writeReg32(dmprogbuf5, 0x90029002); err != nil { | |
| return err | |
| } | |
| } else { | |
| if err := l.writeReg32(dmprogbuf2, 0x9002c184); err != nil { | |
| return err | |
| } | |
| } | |
| if err := l.writeReg32(dmdata1, address); err != nil { | |
| return err | |
| } | |
| if err := l.writeReg32(dmdata0, data); err != nil { | |
| return err | |
| } | |
| if err := l.writeReg32(dmcommand, 0x00240000); err != nil { | |
| return err | |
| } | |
| if didDisableReq { | |
| if err := l.writeReg32(dmabstractauto, 1); err != nil { | |
| return err | |
| } | |
| } | |
| l.lastWriteFlag = isFlash | |
| l.stateTag = stateWriteSeq | |
| l.currentAddr = address | |
| } else { | |
| if address != l.currentAddr { | |
| if err := l.writeReg32(dmdata1, address); err != nil { | |
| return err | |
| } | |
| } | |
| if err := l.writeReg32(dmdata0, data); err != nil { | |
| return err | |
| } | |
| } | |
| if isFlash { | |
| if err := l.waitForDoneOp(false); err != nil { | |
| return err | |
| } | |
| } | |
| l.currentAddr = address + 4 | |
| return nil | |
| } | |
| func (l *linkE) readWord(address uint32) (uint32, error) { | |
| autoincrement := true | |
| if address == flashCTLR || address == flashSTATR { | |
| autoincrement = false | |
| } | |
| if l.stateTag != stateReadSeq || address != l.currentAddr || autoincrement != l.autoIncrement { | |
| if l.stateTag != stateReadSeq || !autoincrement { | |
| if l.stateTag != stateWriteSeq { | |
| if err := l.staticUpdatePROGBUFRegs(); err != nil { | |
| return 0, err | |
| } | |
| } | |
| if err := l.writeReg32(dmabstractauto, 0); err != nil { | |
| return 0, err | |
| } | |
| if err := l.writeReg32(dmprogbuf0, 0x40044180); err != nil { | |
| return 0, err | |
| } | |
| if autoincrement { | |
| if err := l.writeReg32(dmprogbuf1, 0xc1040411); err != nil { | |
| return 0, err | |
| } | |
| } else { | |
| if err := l.writeReg32(dmprogbuf1, 0xc1040001); err != nil { | |
| return 0, err | |
| } | |
| } | |
| if err := l.writeReg32(dmprogbuf2, 0x9002c180); err != nil { | |
| return 0, err | |
| } | |
| if autoincrement { | |
| if err := l.writeReg32(dmabstractauto, 1); err != nil { | |
| return 0, err | |
| } | |
| } | |
| l.autoIncrement = autoincrement | |
| } | |
| if err := l.writeReg32(dmdata1, address); err != nil { | |
| return 0, err | |
| } | |
| if err := l.writeReg32(dmcommand, 0x00240000); err != nil { | |
| return 0, err | |
| } | |
| l.stateTag = stateReadSeq | |
| l.currentAddr = address | |
| if err := l.waitForDoneOp(true); err != nil { | |
| _ = l.voidHighLevelState() | |
| } | |
| } | |
| if l.autoIncrement { | |
| l.currentAddr += 4 | |
| } | |
| value, err := l.readReg32(dmdata0) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return value, nil | |
| } | |
| func (l *linkE) waitForFlash() error { | |
| deadline := time.Now().Add(3 * time.Second) | |
| for time.Now().Before(deadline) { | |
| rw, err := l.readWordRaw(flashSTATR) | |
| if err != nil { | |
| return err | |
| } | |
| if rw&3 == 0 { | |
| if rw&flashStatrWrErr != 0 { | |
| return fmt.Errorf("memory protection error statr=%08x", rw) | |
| } | |
| return nil | |
| } | |
| time.Sleep(100 * time.Microsecond) | |
| } | |
| rw, _ := l.readWordRaw(flashSTATR) | |
| return fmt.Errorf("flash timed out statr=%08x", rw) | |
| } | |
| func (l *linkE) unlockFlash() error { | |
| if l.flashUnlocked { | |
| return nil | |
| } | |
| rw, err := l.readWordRaw(flashCTLR) | |
| if err != nil { | |
| return fmt.Errorf("read flash ctlr: %w", err) | |
| } | |
| if rw&0x8080 != 0 { | |
| for _, step := range []struct { | |
| addr uint32 | |
| value uint32 | |
| }{ | |
| {flashKEYR, flashKey1}, | |
| {flashKEYR, flashKey2}, | |
| {flashOBKEYR, flashKey1}, | |
| {flashOBKEYR, flashKey2}, | |
| {flashMODEKEYR, flashKey1}, | |
| {flashMODEKEYR, flashKey2}, | |
| } { | |
| if err := l.writeWordRaw(step.addr, step.value); err != nil { | |
| return fmt.Errorf("unlock flash write %08x: %w", step.addr, err) | |
| } | |
| } | |
| rw, err = l.readWordRaw(flashCTLR) | |
| if err != nil { | |
| return fmt.Errorf("re-read flash ctlr: %w", err) | |
| } | |
| if rw&0x8080 != 0 { | |
| return fmt.Errorf("flash is not unlocked ctlr=%08x", rw) | |
| } | |
| } | |
| l.flashUnlocked = true | |
| return nil | |
| } | |
| func (l *linkE) eraseSector(address uint32) error { | |
| if err := l.waitForFlash(); err != nil { | |
| return err | |
| } | |
| if err := l.writeWordRaw(flashCTLR, crPageER); err != nil { | |
| return err | |
| } | |
| if err := l.writeWordRaw(flashADDR, address); err != nil { | |
| return err | |
| } | |
| if err := l.writeWordRaw(flashCTLR, crStartSet|crPageER); err != nil { | |
| return err | |
| } | |
| if err := l.voidHighLevelState(); err != nil { | |
| return err | |
| } | |
| return l.waitForFlash() | |
| } | |
| func (l *linkE) writeFlashPage(address uint32, page []byte) error { | |
| if len(page) != sectorSize { | |
| return fmt.Errorf("page size must be %d, got %d", sectorSize, len(page)) | |
| } | |
| if err := l.writeWordRaw(flashCTLR, crPagePG); err != nil { | |
| return err | |
| } | |
| if err := l.writeWordRaw(flashCTLR, crBufRST|crPagePG); err != nil { | |
| return err | |
| } | |
| if err := l.voidHighLevelState(); err != nil { | |
| return err | |
| } | |
| if err := l.waitForFlash(); err != nil { | |
| return err | |
| } | |
| for i := 0; i < len(page); i += 4 { | |
| word := binary.LittleEndian.Uint32(page[i : i+4]) | |
| if err := l.writeWord(address+uint32(i), word); err != nil { | |
| return err | |
| } | |
| } | |
| if err := l.writeWordRaw(flashADDR, address); err != nil { | |
| return err | |
| } | |
| if err := l.writeWordRaw(flashCTLR, crPagePG|crStartSet); err != nil { | |
| return err | |
| } | |
| if err := l.voidHighLevelState(); err != nil { | |
| return err | |
| } | |
| return l.waitForFlash() | |
| } | |
| func (l *linkE) writeReg32(reg uint8, value uint32) error { | |
| req := []byte{ | |
| 0x81, 0x08, 0x06, reg, | |
| byte(value >> 24), byte(value >> 16), byte(value >> 8), byte(value), | |
| 0x02, | |
| } | |
| if err := l.bulkOut(0x01, req); err != nil { | |
| return fmt.Errorf("send register write %02x: %w", reg, err) | |
| } | |
| for attempt := 0; attempt < 4; attempt++ { | |
| reply := make([]byte, 64) | |
| n, err := l.bulkInCount(0x81, reply, commandTimeoutMS) | |
| if err != nil { | |
| return fmt.Errorf("recv register write %02x: %w", reg, err) | |
| } | |
| reply = reply[:n] | |
| if len(reply) == 9 && reply[0] == 0x82 && reply[1] == 0x08 && reply[2] == 0x06 && reply[3] == reg && reply[8] != 0x02 && reply[8] != 0x03 { | |
| return nil | |
| } | |
| } | |
| return fmt.Errorf("bad register write response for reg %02x", reg) | |
| } | |
| func (l *linkE) readReg32(reg uint8) (uint32, error) { | |
| req := []byte{0x81, 0x08, 0x06, reg, 0, 0, 0, 0, 0x01} | |
| if err := l.bulkOut(0x01, req); err != nil { | |
| return 0, fmt.Errorf("send register read %02x: %w", reg, err) | |
| } | |
| for attempt := 0; attempt < 4; attempt++ { | |
| reply := make([]byte, 64) | |
| n, err := l.bulkInCount(0x81, reply, commandTimeoutMS) | |
| if err != nil { | |
| return 0, fmt.Errorf("recv register read %02x: %w", reg, err) | |
| } | |
| reply = reply[:n] | |
| if len(reply) == 9 && reply[0] == 0x82 && reply[1] == 0x08 && reply[2] == 0x06 && reply[3] == reg && reply[8] != 0x02 && reply[8] != 0x03 { | |
| return binary.BigEndian.Uint32(reply[4:8]), nil | |
| } | |
| } | |
| return 0, fmt.Errorf("bad register read response for reg %02x", reg) | |
| } | |
| func bytesRange(data []byte, start, end int) []byte { | |
| if start >= len(data) { | |
| return nil | |
| } | |
| if end > len(data) { | |
| end = len(data) | |
| } | |
| if start < 0 { | |
| start = 0 | |
| } | |
| return data[start:end] | |
| } | |
| func programmerName(id byte) string { | |
| switch id { | |
| case 1: | |
| return "CH549" | |
| case 2: | |
| return "CH32V307" | |
| case 3: | |
| return "CH32V203" | |
| case 4: | |
| return "LinkB" | |
| case 5: | |
| return "LinkW" | |
| case 18: | |
| return "LinkE" | |
| default: | |
| return fmt.Sprintf("unknown-%02x", id) | |
| } | |
| } | |
| func hexBytes(data []byte) string { | |
| if len(data) == 0 { | |
| return "<empty>" | |
| } | |
| parts := make([]string, len(data)) | |
| for i, b := range data { | |
| parts[i] = fmt.Sprintf("%02x", b) | |
| } | |
| return strings.Join(parts, " ") | |
| } |